133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import { Component, type JSX, type ReactNode } from 'react';
|
|
|
|
import { useAppTranslation } from '@features/localization/renderer';
|
|
import { AlertTriangle } from 'lucide-react';
|
|
|
|
interface DiffErrorBoundaryProps {
|
|
children: ReactNode;
|
|
filePath: string;
|
|
oldString?: string;
|
|
newString?: string;
|
|
onRetry?: () => void;
|
|
}
|
|
|
|
interface DiffErrorBoundaryState {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
type ReviewT = ReturnType<typeof useAppTranslation>['t'];
|
|
|
|
interface DiffErrorBoundaryInnerProps extends DiffErrorBoundaryProps {
|
|
t: ReviewT;
|
|
}
|
|
|
|
class DiffErrorBoundaryInner extends Component<
|
|
DiffErrorBoundaryInnerProps,
|
|
DiffErrorBoundaryState
|
|
> {
|
|
constructor(props: DiffErrorBoundaryInnerProps) {
|
|
super(props);
|
|
this.state = { hasError: false, error: null };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): DiffErrorBoundaryState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
console.error(
|
|
'[DiffErrorBoundary] Error rendering diff for',
|
|
this.props.filePath,
|
|
error,
|
|
errorInfo
|
|
);
|
|
}
|
|
|
|
render(): JSX.Element {
|
|
if (!this.state.hasError) {
|
|
return this.props.children as JSX.Element;
|
|
}
|
|
|
|
const { filePath, oldString, newString, onRetry, t } = this.props;
|
|
const { error } = this.state;
|
|
|
|
return (
|
|
<div className="m-4 rounded-lg border border-red-500/20 bg-red-500/10 p-4">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<AlertTriangle className="size-4 text-red-400" />
|
|
<span className="text-sm font-medium text-red-300">{t('review.diffError.title')}</span>
|
|
</div>
|
|
|
|
<p className="mb-3 text-xs text-red-300/80">
|
|
{error?.message ?? t('review.diffError.unexpected')}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{onRetry && (
|
|
<button
|
|
onClick={() => {
|
|
this.setState({ hasError: false, error: null });
|
|
onRetry();
|
|
}}
|
|
className="rounded bg-red-500/20 px-3 py-1 text-xs text-red-300 transition-colors hover:bg-red-500/30"
|
|
>
|
|
{t('review.diffError.actions.retry')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{(oldString || newString) && (
|
|
<details className="mt-3">
|
|
<summary className="cursor-pointer text-xs text-red-300/60 hover:text-red-300/80">
|
|
{t('review.diffError.raw.show')}
|
|
</summary>
|
|
<div className="mt-2 max-h-60 overflow-auto rounded bg-surface p-2 font-mono text-xs text-text-muted">
|
|
<div className="mb-1 text-text-secondary">
|
|
{t('review.diffError.raw.file', { file: filePath })}
|
|
</div>
|
|
{oldString && (
|
|
<div className="mb-2">
|
|
<div className="mb-0.5 text-red-400">{t('review.diffError.raw.original')}</div>
|
|
<pre className="whitespace-pre-wrap">{oldString.slice(0, 2000)}</pre>
|
|
{oldString.length > 2000 && (
|
|
<span className="text-text-muted">
|
|
{t('review.diffError.raw.charsTotal', { count: oldString.length })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
{newString && (
|
|
<div>
|
|
<div className="mb-0.5 text-green-400">{t('review.diffError.raw.modified')}</div>
|
|
<pre className="whitespace-pre-wrap">{newString.slice(0, 2000)}</pre>
|
|
{newString.length > 2000 && (
|
|
<span className="text-text-muted">
|
|
{t('review.diffError.raw.charsTotal', { count: newString.length })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export const DiffErrorBoundary = (props: DiffErrorBoundaryProps): JSX.Element => {
|
|
const { t } = useAppTranslation('team');
|
|
const { children, filePath, newString, oldString, onRetry } = props;
|
|
return (
|
|
<DiffErrorBoundaryInner
|
|
filePath={filePath}
|
|
newString={newString}
|
|
oldString={oldString}
|
|
onRetry={onRetry}
|
|
t={t}
|
|
>
|
|
{children}
|
|
</DiffErrorBoundaryInner>
|
|
);
|
|
};
|