Yeuoly b76e17b25d
feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 17:59:37 +08:00

274 lines
7.9 KiB
TypeScript

'use client'
import type { ErrorInfo, ReactNode } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RiAlertLine, RiBugLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type ErrorBoundaryState = {
hasError: boolean
error: Error | null
errorInfo: ErrorInfo | null
errorCount: number
}
type ErrorBoundaryProps = {
children: ReactNode
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
onError?: (error: Error, errorInfo: ErrorInfo) => void
onReset?: () => void
showDetails?: boolean
className?: string
resetKeys?: Array<string | number>
resetOnPropsChange?: boolean
isolate?: boolean
enableRecovery?: boolean
customTitle?: string
customMessage?: string
}
// Internal class component for error catching
class ErrorBoundaryInner extends React.Component<
ErrorBoundaryProps & {
resetErrorBoundary: () => void
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
},
ErrorBoundaryState
> {
constructor(props: any) {
super(props)
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0,
}
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error,
}
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
if (process.env.NODE_ENV === 'development') {
console.error('ErrorBoundary caught an error:', error)
console.error('Error Info:', errorInfo)
}
this.setState(prevState => ({
errorInfo,
errorCount: prevState.errorCount + 1,
}))
if (this.props.onError)
this.props.onError(error, errorInfo)
}
componentDidUpdate(prevProps: any) {
const { resetKeys, resetOnPropsChange } = this.props
const { hasError } = this.state
if (hasError && prevProps.resetKeys !== resetKeys) {
if (resetKeys?.some((key, idx) => key !== prevProps.resetKeys?.[idx]))
this.props.resetErrorBoundary()
}
if (hasError && resetOnPropsChange && prevProps.children !== this.props.children)
this.props.resetErrorBoundary()
if (prevProps.resetKeys !== resetKeys)
this.props.onResetKeysChange(prevProps.resetKeys)
}
render() {
const { hasError, error, errorInfo, errorCount } = this.state
const {
fallback,
children,
showDetails = false,
className,
isolate = true,
enableRecovery = true,
customTitle,
customMessage,
resetErrorBoundary,
} = this.props
if (hasError && error) {
if (fallback) {
if (typeof fallback === 'function')
return fallback(error, resetErrorBoundary)
return fallback
}
return (
<div
className={cn(
'border-state-critical-border bg-state-critical-hover-alt flex flex-col items-center justify-center rounded-lg border p-8',
isolate && 'min-h-[200px]',
className,
)}
>
<div className='mb-4 flex items-center gap-2'>
<RiAlertLine className='text-state-critical-solid h-8 w-8' />
<h2 className='text-xl font-semibold text-text-primary'>
{customTitle || 'Something went wrong'}
</h2>
</div>
<p className='mb-6 text-center text-text-secondary'>
{customMessage || 'An unexpected error occurred while rendering this component.'}
</p>
{showDetails && errorInfo && (
<details className='mb-6 w-full max-w-2xl'>
<summary className='mb-2 cursor-pointer text-sm font-medium text-text-tertiary hover:text-text-secondary'>
<span className='inline-flex items-center gap-1'>
<RiBugLine className='h-4 w-4' />
Error Details (Development Only)
</span>
</summary>
<div className='rounded-lg bg-gray-100 p-4'>
<div className='mb-2'>
<span className='font-mono text-xs font-semibold text-gray-600'>Error:</span>
<pre className='mt-1 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-800'>
{error.toString()}
</pre>
</div>
{errorInfo && (
<div>
<span className='font-mono text-xs font-semibold text-gray-600'>Component Stack:</span>
<pre className='mt-1 max-h-40 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-700'>
{errorInfo.componentStack}
</pre>
</div>
)}
{errorCount > 1 && (
<div className='mt-2 text-xs text-gray-600'>
This error has occurred {errorCount} times
</div>
)}
</div>
</details>
)}
{enableRecovery && (
<div className='flex gap-3'>
<Button
variant='primary'
size='small'
onClick={resetErrorBoundary}
>
Try Again
</Button>
<Button
variant='secondary'
size='small'
onClick={() => window.location.reload()}
>
Reload Page
</Button>
</div>
)}
</div>
)
}
return children
}
}
// Main functional component wrapper
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
const resetKeysRef = useRef(props.resetKeys)
const prevResetKeysRef = useRef<Array<string | number> | undefined>(undefined)
const resetErrorBoundary = useCallback(() => {
setErrorBoundaryKey(prev => prev + 1)
props.onReset?.()
}, [props])
const onResetKeysChange = useCallback((prevResetKeys?: Array<string | number>) => {
prevResetKeysRef.current = prevResetKeys
}, [])
useEffect(() => {
if (prevResetKeysRef.current !== props.resetKeys)
resetKeysRef.current = props.resetKeys
}, [props.resetKeys])
return (
<ErrorBoundaryInner
{...props}
key={errorBoundaryKey}
resetErrorBoundary={resetErrorBoundary}
onResetKeysChange={onResetKeysChange}
/>
)
}
// Hook for imperative error handling
export function useErrorHandler() {
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
if (error)
throw error
}, [error])
return setError
}
// Hook for catching async errors
export function useAsyncError() {
const [, setError] = useState()
return useCallback(
(error: Error) => {
setError(() => {
throw error
})
},
[setError],
)
}
// HOC for wrapping components with error boundary
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
errorBoundaryProps?: Omit<ErrorBoundaryProps, 'children'>,
): React.ComponentType<P> {
const WrappedComponent = (props: P) => (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
)
WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name || 'Component'})`
return WrappedComponent
}
// Simple error fallback component
export const ErrorFallback: React.FC<{
error: Error
resetErrorBoundary: () => void
}> = ({ error, resetErrorBoundary }) => {
return (
<div className='flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-8'>
<h2 className='mb-2 text-lg font-semibold text-red-800'>Oops! Something went wrong</h2>
<p className='mb-4 text-center text-red-600'>{error.message}</p>
<Button onClick={resetErrorBoundary} size='small'>
Try again
</Button>
</div>
)
}
export default ErrorBoundary