mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
fix issue
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Button, Group, List, Modal, Stack, Text } from "@mantine/core";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Global API error modal.
|
||||
*
|
||||
* The axios client emits every failed request (with a server response) through
|
||||
* `emitApiError`; this modal — mounted once at the app root — shows the
|
||||
* SERVER'S actual `message` instead of a generic "Request failed with status
|
||||
* code NNN". Pages listed in EXCLUDED_PATH_PATTERNS (warehouse, first/last
|
||||
* mile, onboarding/auth screens) keep their own inline error handling and
|
||||
* never trigger it.
|
||||
*/
|
||||
|
||||
export interface ApiErrorPayload {
|
||||
/** Server messages — the `message` field (string or class-validator array). */
|
||||
messages: string[];
|
||||
statusCode?: number;
|
||||
/** API path that failed, shown as small helper text. */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
type Listener = (payload: ApiErrorPayload) => void;
|
||||
|
||||
let listener: Listener | null = null;
|
||||
|
||||
/** Current-page path patterns where the global modal must stay silent. */
|
||||
const EXCLUDED_PATH_PATTERNS = [
|
||||
/^\/auth/,
|
||||
/^\/callback/,
|
||||
/warehouse/i,
|
||||
/first-mile/i,
|
||||
/last-mile/i,
|
||||
/onboard/i,
|
||||
/register/i,
|
||||
];
|
||||
|
||||
export function isGlobalErrorModalSuppressed(pathname: string): boolean {
|
||||
return EXCLUDED_PATH_PATTERNS.some((re) => re.test(pathname));
|
||||
}
|
||||
|
||||
export function emitApiError(payload: ApiErrorPayload): void {
|
||||
if (isGlobalErrorModalSuppressed(window.location.pathname)) return;
|
||||
listener?.(payload);
|
||||
}
|
||||
|
||||
/** Pull the server `message` out of an axios-style error. */
|
||||
export function extractApiErrorPayload(error: unknown): ApiErrorPayload | null {
|
||||
const err = error as {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
message?: string | string[];
|
||||
error?: string;
|
||||
statusCode?: number;
|
||||
path?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
const response = err?.response;
|
||||
if (!response) return null; // network error / cancellation — not ours
|
||||
const data = response.data;
|
||||
const raw = data?.message;
|
||||
const messages = Array.isArray(raw)
|
||||
? raw.filter((m): m is string => typeof m === "string" && m.length > 0)
|
||||
: typeof raw === "string" && raw.length > 0
|
||||
? [raw]
|
||||
: [];
|
||||
if (messages.length === 0) {
|
||||
messages.push(
|
||||
data?.error ?? `Request failed with status code ${response.status}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
messages,
|
||||
statusCode: data?.statusCode ?? response.status,
|
||||
path: data?.path,
|
||||
};
|
||||
}
|
||||
|
||||
export function ApiErrorModal() {
|
||||
const [payload, setPayload] = useState<ApiErrorPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listener = (next) => {
|
||||
// Don't stack identical messages while the modal is already showing them.
|
||||
setPayload((current) =>
|
||||
current && current.messages.join("\n") === next.messages.join("\n")
|
||||
? current
|
||||
: next,
|
||||
);
|
||||
};
|
||||
return () => {
|
||||
listener = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const close = () => setPayload(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={payload !== null}
|
||||
onClose={close}
|
||||
centered
|
||||
radius="md"
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<AlertTriangle size={18} color="var(--mantine-color-red-6)" />
|
||||
<Text fw={700}>Request failed</Text>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
{payload && (
|
||||
<Stack gap="md">
|
||||
<Alert color="red" variant="light">
|
||||
{payload.messages.length === 1 ? (
|
||||
<Text size="sm">{payload.messages[0]}</Text>
|
||||
) : (
|
||||
<List size="sm" spacing={4}>
|
||||
{payload.messages.map((m, i) => (
|
||||
<List.Item key={i}>{m}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Alert>
|
||||
{(payload.statusCode || payload.path) && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{payload.statusCode ? `Status ${payload.statusCode}` : null}
|
||||
{payload.statusCode && payload.path ? " · " : null}
|
||||
{payload.path}
|
||||
</Text>
|
||||
)}
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={close}>
|
||||
Close
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user