mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 20:46:44 +03:00
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:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.9.2",
|
"version": "2.9.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '../ui/dialog';
|
} from '../ui/dialog';
|
||||||
import { apiRequest, formatDate } from '@/lib/utils';
|
import { apiRequest, formatDate, showErrorToast } from '@/lib/utils';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import type { MirrorJob } from '@/lib/db/schema';
|
import type { MirrorJob } from '@/lib/db/schema';
|
||||||
import type { ActivityApiResponse } from '@/types/activities';
|
import type { ActivityApiResponse } from '@/types/activities';
|
||||||
@@ -155,7 +155,7 @@ export function ActivityLog() {
|
|||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
// Only show error toast for manual refreshes to avoid spam during live updates
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
if (!isLiveRefresh) {
|
if (!isLiveRefresh) {
|
||||||
toast.error(res.message ?? 'Failed to fetch activities.');
|
showErrorToast(res.message ?? 'Failed to fetch activities.', toast);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -184,9 +184,7 @@ export function ActivityLog() {
|
|||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
// Only show error toast for manual refreshes to avoid spam during live updates
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
if (!isLiveRefresh) {
|
if (!isLiveRefresh) {
|
||||||
toast.error(
|
showErrorToast(err, toast);
|
||||||
err instanceof Error ? err.message : 'Failed to fetch activities.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -331,11 +329,11 @@ export function ActivityLog() {
|
|||||||
setActivities([]);
|
setActivities([]);
|
||||||
toast.success(`All activities cleaned up successfully. Deleted ${res.result.mirrorJobsDeleted} mirror jobs and ${res.result.eventsDeleted} events.`);
|
toast.success(`All activities cleaned up successfully. Deleted ${res.result.mirrorJobsDeleted} mirror jobs and ${res.result.eventsDeleted} events.`);
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.error || 'Failed to cleanup activities.');
|
showErrorToast(res.error || 'Failed to cleanup activities.', toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cleaning up activities:', error);
|
console.error('Error cleaning up activities:', error);
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to cleanup activities.');
|
showErrorToast(error, toast);
|
||||||
} finally {
|
} finally {
|
||||||
setIsInitialLoading(false);
|
setIsInitialLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { SiGitea } from 'react-icons/si';
|
import { SiGitea } from 'react-icons/si';
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
|
import { showErrorToast } from '@/lib/utils';
|
||||||
import { FlipHorizontal } from 'lucide-react';
|
import { FlipHorizontal } from 'lucide-react';
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
@@ -45,10 +46,10 @@ export function LoginForm() {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || 'Login failed. Please try again.');
|
showErrorToast(data.error || 'Login failed. Please try again.', toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('An error occurred while logging in. Please try again.');
|
showErrorToast(error, toast);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { GitMerge } from 'lucide-react';
|
import { GitMerge } from 'lucide-react';
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
|
import { showErrorToast } from '@/lib/utils';
|
||||||
|
|
||||||
export function SignupForm() {
|
export function SignupForm() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -51,10 +52,10 @@ export function SignupForm() {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
toast.error('An error occurred while creating your account. Please try again.');
|
showErrorToast(error, toast);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
} from '@/types/config';
|
} from '@/types/config';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { apiRequest } from '@/lib/utils';
|
import { apiRequest, showErrorToast } from '@/lib/utils';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
@@ -134,16 +134,13 @@ export function ConfigTabs() {
|
|||||||
'Configuration saved successfully! Now import your GitHub data to begin.',
|
'Configuration saved successfully! Now import your GitHub data to begin.',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
showErrorToast(
|
||||||
`Failed to save configuration: ${result.message || 'Unknown error'}`,
|
`Failed to save configuration: ${result.message || 'Unknown error'}`,
|
||||||
|
toast
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
`An error occurred while saving the configuration: ${
|
|
||||||
error instanceof Error ? error.message : String(error)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,18 +195,13 @@ export function ConfigTabs() {
|
|||||||
console.warn('Failed to fetch updated config after auto-save:', fetchError);
|
console.warn('Failed to fetch updated config after auto-save:', fetchError);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
showErrorToast(
|
||||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
{ duration: 3000 }
|
toast
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
`Auto-save error: ${
|
|
||||||
error instanceof Error ? error.message : String(error)
|
|
||||||
}`,
|
|
||||||
{ duration: 3000 }
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsAutoSavingSchedule(false);
|
setIsAutoSavingSchedule(false);
|
||||||
}
|
}
|
||||||
@@ -266,18 +258,13 @@ export function ConfigTabs() {
|
|||||||
console.warn('Failed to fetch updated config after auto-save:', fetchError);
|
console.warn('Failed to fetch updated config after auto-save:', fetchError);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
showErrorToast(
|
||||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
{ duration: 3000 }
|
toast
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
`Auto-save error: ${
|
|
||||||
error instanceof Error ? error.message : String(error)
|
|
||||||
}`,
|
|
||||||
{ duration: 3000 }
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsAutoSavingCleanup(false);
|
setIsAutoSavingCleanup(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GitFork, Clock, FlipHorizontal, Building2 } from "lucide-react";
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { MirrorJob, Organization, Repository } from "@/lib/db/schema";
|
import type { MirrorJob, Organization, Repository } from "@/lib/db/schema";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiRequest } from "@/lib/utils";
|
import { apiRequest, showErrorToast } from "@/lib/utils";
|
||||||
import type { DashboardApiResponse } from "@/types/dashboard";
|
import type { DashboardApiResponse } from "@/types/dashboard";
|
||||||
import { useSSE } from "@/hooks/useSEE";
|
import { useSSE } from "@/hooks/useSEE";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -103,15 +103,11 @@ export function Dashboard() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error fetching dashboard data");
|
showErrorToast(response.error || "Error fetching dashboard data", toast);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Error fetching dashboard data"
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { MirrorJob, Organization } from "@/lib/db/schema";
|
|||||||
import { OrganizationList } from "./OrganizationsList";
|
import { OrganizationList } from "./OrganizationsList";
|
||||||
import AddOrganizationDialog from "./AddOrganizationDialog";
|
import AddOrganizationDialog from "./AddOrganizationDialog";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { apiRequest } from "@/lib/utils";
|
import { apiRequest, showErrorToast } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
membershipRoleEnum,
|
membershipRoleEnum,
|
||||||
type AddOrganizationApiRequest,
|
type AddOrganizationApiRequest,
|
||||||
@@ -193,12 +193,10 @@ export function Organization() {
|
|||||||
searchTerm: org,
|
searchTerm: org,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error adding organization");
|
showErrorToast(response.error || "Error adding organization", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error adding organization"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -250,12 +248,10 @@ export function Organization() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting mirror jobs");
|
showErrorToast(response.error || "Error starting mirror jobs", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error starting mirror jobs"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Reset loading states - we'll let the SSE updates handle status changes
|
// Reset loading states - we'll let the SSE updates handle status changes
|
||||||
setLoadingOrgIds(new Set());
|
setLoadingOrgIds(new Set());
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type RepositoryApiResponse,
|
type RepositoryApiResponse,
|
||||||
type RepoStatus,
|
type RepoStatus,
|
||||||
} from "@/types/Repository";
|
} from "@/types/Repository";
|
||||||
import { apiRequest } from "@/lib/utils";
|
import { apiRequest, showErrorToast } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -108,16 +108,14 @@ export default function Repository() {
|
|||||||
} else {
|
} else {
|
||||||
// Only show error toast for manual refreshes to avoid spam during live updates
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
if (!isLiveRefresh) {
|
if (!isLiveRefresh) {
|
||||||
toast.error(response.error || "Error fetching repositories");
|
showErrorToast(response.error || "Error fetching repositories", toast);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only show error toast for manual refreshes to avoid spam during live updates
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
if (!isLiveRefresh) {
|
if (!isLiveRefresh) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error fetching repositories"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,12 +182,10 @@ export default function Repository() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting mirror job");
|
showErrorToast(response.error || "Error starting mirror job", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error starting mirror job"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRepoIds((prev) => {
|
setLoadingRepoIds((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -248,12 +244,10 @@ export default function Repository() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting mirror jobs");
|
showErrorToast(response.error || "Error starting mirror jobs", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error starting mirror jobs"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Reset loading states - we'll let the SSE updates handle status changes
|
// Reset loading states - we'll let the SSE updates handle status changes
|
||||||
setLoadingRepoIds(new Set());
|
setLoadingRepoIds(new Set());
|
||||||
@@ -287,12 +281,10 @@ export default function Repository() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting sync job");
|
showErrorToast(response.error || "Error starting sync job", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error starting sync job"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRepoIds((prev) => {
|
setLoadingRepoIds((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -329,12 +321,10 @@ export default function Repository() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error retrying job");
|
showErrorToast(response.error || "Error retrying job", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error retrying job"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRepoIds((prev) => {
|
setLoadingRepoIds((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
@@ -381,12 +371,10 @@ export default function Repository() {
|
|||||||
searchTerm: repo,
|
searchTerm: repo,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error adding repository");
|
showErrorToast(response.error || "Error adding repository", toast);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
showErrorToast(error, toast);
|
||||||
error instanceof Error ? error.message : "Error adding repository"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { jsonResponse, formatDate, truncate, safeParse } from "./utils";
|
import { jsonResponse, formatDate, truncate, safeParse, parseErrorMessage, showErrorToast } from "./utils";
|
||||||
|
|
||||||
describe("jsonResponse", () => {
|
describe("jsonResponse", () => {
|
||||||
test("creates a Response with JSON content", () => {
|
test("creates a Response with JSON content", () => {
|
||||||
@@ -108,3 +108,53 @@ describe("safeParse", () => {
|
|||||||
expect(parsed).toBe(obj);
|
expect(parsed).toBe(obj);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parseErrorMessage", () => {
|
||||||
|
test("parses JSON error with error and troubleshooting fields", () => {
|
||||||
|
const errorMessage = JSON.stringify({
|
||||||
|
error: "Unexpected end of JSON input",
|
||||||
|
errorType: "SyntaxError",
|
||||||
|
timestamp: "2025-05-28T09:08:02.37Z",
|
||||||
|
troubleshooting: "JSON parsing error detected. Check Gitea server status and logs. Ensure Gitea is returning valid JSON responses."
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseErrorMessage(errorMessage);
|
||||||
|
|
||||||
|
expect(result.title).toBe("Unexpected end of JSON input");
|
||||||
|
expect(result.description).toBe("JSON parsing error detected. Check Gitea server status and logs. Ensure Gitea is returning valid JSON responses.");
|
||||||
|
expect(result.isStructured).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses JSON error with title and description fields", () => {
|
||||||
|
const errorMessage = JSON.stringify({
|
||||||
|
title: "Connection Failed",
|
||||||
|
description: "Unable to connect to the server. Please check your network connection."
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseErrorMessage(errorMessage);
|
||||||
|
|
||||||
|
expect(result.title).toBe("Connection Failed");
|
||||||
|
expect(result.description).toBe("Unable to connect to the server. Please check your network connection.");
|
||||||
|
expect(result.isStructured).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles plain string error messages", () => {
|
||||||
|
const errorMessage = "Simple error message";
|
||||||
|
|
||||||
|
const result = parseErrorMessage(errorMessage);
|
||||||
|
|
||||||
|
expect(result.title).toBe("Simple error message");
|
||||||
|
expect(result.description).toBeUndefined();
|
||||||
|
expect(result.isStructured).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles Error objects", () => {
|
||||||
|
const error = new Error("Something went wrong");
|
||||||
|
|
||||||
|
const result = parseErrorMessage(error);
|
||||||
|
|
||||||
|
expect(result.title).toBe("Something went wrong");
|
||||||
|
expect(result.description).toBeUndefined();
|
||||||
|
expect(result.isStructured).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
131
src/lib/utils.ts
131
src/lib/utils.ts
@@ -36,20 +36,141 @@ export function safeParse<T>(value: unknown): T | undefined {
|
|||||||
return value as T;
|
return value as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced error message parsing for toast notifications
|
||||||
|
export interface ParsedErrorMessage {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
isStructured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseErrorMessage(error: unknown): ParsedErrorMessage {
|
||||||
|
// Handle Error objects
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return parseErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string messages
|
||||||
|
if (typeof error === "string") {
|
||||||
|
// Try to parse as JSON first
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(error);
|
||||||
|
|
||||||
|
// Check for common structured error formats
|
||||||
|
if (typeof parsed === "object" && parsed !== null) {
|
||||||
|
// Format 1: { error: "message", errorType: "type", troubleshooting: "info" }
|
||||||
|
if (parsed.error) {
|
||||||
|
return {
|
||||||
|
title: parsed.error,
|
||||||
|
description: parsed.troubleshooting || parsed.errorType || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 2: { title: "title", description: "desc" }
|
||||||
|
if (parsed.title) {
|
||||||
|
return {
|
||||||
|
title: parsed.title,
|
||||||
|
description: parsed.description || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 3: { message: "msg", details: "details" }
|
||||||
|
if (parsed.message) {
|
||||||
|
return {
|
||||||
|
title: parsed.message,
|
||||||
|
description: parsed.details || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, treat as plain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain string message
|
||||||
|
return {
|
||||||
|
title: error,
|
||||||
|
description: undefined,
|
||||||
|
isStructured: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle objects directly
|
||||||
|
if (typeof error === "object" && error !== null) {
|
||||||
|
const errorObj = error as any;
|
||||||
|
|
||||||
|
if (errorObj.error) {
|
||||||
|
return {
|
||||||
|
title: errorObj.error,
|
||||||
|
description: errorObj.troubleshooting || errorObj.errorType || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorObj.title) {
|
||||||
|
return {
|
||||||
|
title: errorObj.title,
|
||||||
|
description: errorObj.description || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorObj.message) {
|
||||||
|
return {
|
||||||
|
title: errorObj.message,
|
||||||
|
description: errorObj.details || undefined,
|
||||||
|
isStructured: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for unknown types
|
||||||
|
return {
|
||||||
|
title: String(error),
|
||||||
|
description: undefined,
|
||||||
|
isStructured: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced toast helper that parses structured error messages
|
||||||
|
export function showErrorToast(error: unknown, toast: any) {
|
||||||
|
const parsed = parseErrorMessage(error);
|
||||||
|
|
||||||
|
if (parsed.description) {
|
||||||
|
// Use sonner's rich toast format with title and description
|
||||||
|
toast.error(parsed.title, {
|
||||||
|
description: parsed.description,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Simple error toast
|
||||||
|
toast.error(parsed.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function for API requests
|
// Helper function for API requests
|
||||||
|
|
||||||
export async function apiRequest<T>(
|
export async function apiRequest<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {}
|
options: (RequestInit & { data?: any }) = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const response = await httpRequest<T>(`${API_BASE}${endpoint}`, {
|
// Handle the custom 'data' property by converting it to 'body'
|
||||||
|
const { data, ...requestOptions } = options;
|
||||||
|
const finalOptions: RequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(options.headers || {}),
|
...(requestOptions.headers || {}),
|
||||||
},
|
},
|
||||||
...options,
|
...requestOptions,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// If data is provided, stringify it and set as body
|
||||||
|
if (data !== undefined) {
|
||||||
|
finalOptions.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await httpRequest<T>(`${API_BASE}${endpoint}`, finalOptions);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user