mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
fix: Add error boundary to handle React-Quill DOM manipulation errors
Wraps RichTextEditor component with an error boundary to gracefully handle "removeChild" errors that occur when React and Quill's DOM get out of sync. This prevents app crashes when switching between diagrams or unmounting nodes. The error boundary: - Catches specific DOM manipulation errors (removeChild, insertBefore, appendChild) - Automatically attempts recovery by re-rendering - Prevents infinite loops with error counting - Shows fallback UI after multiple consecutive errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactQuill from 'react-quill-new';
|
||||
import { Box } from '@mui/material';
|
||||
import RichTextEditorErrorBoundary from './RichTextEditorErrorBoundary';
|
||||
|
||||
interface Props {
|
||||
value?: string;
|
||||
@@ -55,44 +56,46 @@ export const RichTextEditor = ({
|
||||
}, [readOnly]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
'.ql-toolbar.ql-snow': {
|
||||
border: 'none',
|
||||
pt: 0,
|
||||
px: 0,
|
||||
pb: 1 // Add padding below toolbar to prevent overlap, might remove or make configurable at some point
|
||||
},
|
||||
'.ql-toolbar.ql-snow + .ql-container.ql-snow': {
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
borderTop: 'auto',
|
||||
borderRadius: 1.5,
|
||||
height,
|
||||
color: 'text.secondary'
|
||||
},
|
||||
'.ql-container.ql-snow': {
|
||||
...(readOnly ? { border: 'none' } : {}),
|
||||
...styles
|
||||
},
|
||||
'.ql-editor': {
|
||||
whiteSpace: 'pre-wrap', // Preserve multiple spaces and tabs
|
||||
...(readOnly ? { p: 0 } : {}),
|
||||
padding: '12px 15px' // Add consistent padding to prevent text overlap with tooltips
|
||||
},
|
||||
'.ql-tooltip': {
|
||||
zIndex: 1000 // Ensure tooltips appear above content but don't obscure text
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={value ?? ''}
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
formats={formats}
|
||||
modules={modules}
|
||||
/>
|
||||
</Box>
|
||||
<RichTextEditorErrorBoundary>
|
||||
<Box
|
||||
sx={{
|
||||
'.ql-toolbar.ql-snow': {
|
||||
border: 'none',
|
||||
pt: 0,
|
||||
px: 0,
|
||||
pb: 1 // Add padding below toolbar to prevent overlap, might remove or make configurable at some point
|
||||
},
|
||||
'.ql-toolbar.ql-snow + .ql-container.ql-snow': {
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
borderTop: 'auto',
|
||||
borderRadius: 1.5,
|
||||
height,
|
||||
color: 'text.secondary'
|
||||
},
|
||||
'.ql-container.ql-snow': {
|
||||
...(readOnly ? { border: 'none' } : {}),
|
||||
...styles
|
||||
},
|
||||
'.ql-editor': {
|
||||
whiteSpace: 'pre-wrap', // Preserve multiple spaces and tabs
|
||||
...(readOnly ? { p: 0 } : {}),
|
||||
padding: '12px 15px' // Add consistent padding to prevent text overlap with tooltips
|
||||
},
|
||||
'.ql-tooltip': {
|
||||
zIndex: 1000 // Ensure tooltips appear above content but don't obscure text
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={value ?? ''}
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
formats={formats}
|
||||
modules={modules}
|
||||
/>
|
||||
</Box>
|
||||
</RichTextEditorErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
errorCount: number;
|
||||
}
|
||||
|
||||
class RichTextEditorErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
errorCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> | null {
|
||||
// Check if this is the specific DOM manipulation error we're trying to handle
|
||||
if (
|
||||
error.message.includes('removeChild') ||
|
||||
error.message.includes('insertBefore') ||
|
||||
error.message.includes('appendChild')
|
||||
) {
|
||||
// Return state update to trigger re-render
|
||||
return {
|
||||
hasError: true,
|
||||
errorCount: 0
|
||||
};
|
||||
}
|
||||
// For other errors, let them propagate
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
// Log the error for debugging purposes
|
||||
if (error.message.includes('removeChild') ||
|
||||
error.message.includes('insertBefore') ||
|
||||
error.message.includes('appendChild')) {
|
||||
console.warn('RichTextEditor DOM manipulation error caught and handled:', {
|
||||
message: error.message,
|
||||
componentStack: errorInfo.componentStack
|
||||
});
|
||||
|
||||
// Prevent infinite error loops by tracking error count
|
||||
this.setState(prevState => ({
|
||||
errorCount: prevState.errorCount + 1
|
||||
}));
|
||||
|
||||
// If we get too many errors in a row, show fallback
|
||||
if (this.state.errorCount > 3) {
|
||||
console.error('Too many RichTextEditor errors, showing fallback');
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule a recovery attempt after the current render cycle
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
hasError: false
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
|
||||
// Reset error state if we successfully rendered after an error
|
||||
if (prevState.hasError && !this.state.hasError) {
|
||||
this.setState({ errorCount: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError && this.state.errorCount > 3) {
|
||||
// If too many errors, show fallback or placeholder
|
||||
return this.props.fallback || (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
color: '#666'
|
||||
}}>
|
||||
Rich text editor temporarily unavailable
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal render or retry after error
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default RichTextEditorErrorBoundary;
|
||||
Reference in New Issue
Block a user