mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 01:15:24 +08:00
fix(web): unify load error states and retry actions
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import LoadErrorState from '@/components/LoadErrorState';
|
||||
|
||||
export default function EntityLoadState({
|
||||
error = false,
|
||||
@@ -10,27 +10,15 @@ export default function EntityLoadState({
|
||||
onRetry?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (error) return <LoadErrorState onRetry={onRetry} className="flex-1" />;
|
||||
return (
|
||||
<div
|
||||
role={error ? 'alert' : 'status'}
|
||||
aria-busy={!error}
|
||||
role="status"
|
||||
aria-busy="true"
|
||||
className="flex min-h-40 flex-1 flex-col items-center justify-center gap-3 p-6 text-sm text-muted-foreground"
|
||||
>
|
||||
{error ? (
|
||||
<>
|
||||
<p>{t('common.loadFailed')}</p>
|
||||
{onRetry && (
|
||||
<Button type="button" variant="outline" onClick={onRetry}>
|
||||
{t('common.retry')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 aria-hidden="true" className="size-5 animate-spin" />
|
||||
<p>{t('common.loading')}</p>
|
||||
</>
|
||||
)}
|
||||
<Loader2 aria-hidden="true" className="size-5 animate-spin" />
|
||||
<p>{t('common.loading')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { FileWarning, RefreshCw } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function LoadErrorState({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
onRetry,
|
||||
busy = false,
|
||||
compact = false,
|
||||
className,
|
||||
}: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: ReactNode;
|
||||
onRetry?: () => unknown;
|
||||
busy?: boolean;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
|
||||
async function retry() {
|
||||
if (!onRetry || retrying || busy) return;
|
||||
setRetrying(true);
|
||||
try {
|
||||
await onRetry();
|
||||
} catch {
|
||||
// The owning view retains its error state if the request fails again.
|
||||
} finally {
|
||||
setRetrying(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-busy={retrying || busy}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-center px-4',
|
||||
compact ? 'py-4' : 'min-h-64 py-10',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-md items-start gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm sm:p-6">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-amber-500/10 text-amber-600 dark:text-amber-400">
|
||||
{icon ?? <FileWarning className="size-5" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<p className="break-words text-sm font-medium leading-6">
|
||||
{title ?? t('common.loadFailedTitle')}
|
||||
</p>
|
||||
<p className="break-words text-sm leading-6 text-muted-foreground">
|
||||
{description ?? t('common.loadFailedHint')}
|
||||
</p>
|
||||
{onRetry && (
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={retrying || busy}
|
||||
onClick={() => void retry()}
|
||||
>
|
||||
<RefreshCw
|
||||
aria-hidden="true"
|
||||
className={cn('size-3.5', (retrying || busy) && 'animate-spin motion-reduce:animate-none')}
|
||||
/>
|
||||
{retrying || busy ? t('common.loading') : t('common.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user