refactor: improve drag-and-drop handling and cleanup in FileTreeItem and ClaudeOAuthFlow components

- Added useEffect in FileTreeItem to clean up custom drag image on component unmount, preventing memory leaks.
- Enhanced drag image creation using safe DOM manipulation instead of innerHTML.
- Updated ClaudeOAuthFlow to manage auto-advance timeout with cleanup on unmount, ensuring onSuccess is not called after component unmount.

These changes enhance the reliability and performance of drag-and-drop functionality and OAuth flow handling.
This commit is contained in:
AndyMik90
2025-12-20 01:26:23 +01:00
parent 52e12d8d2a
commit 3efab867c5
2 changed files with 37 additions and 9 deletions
@@ -1,4 +1,4 @@
import { useState, useRef, type DragEvent } from 'react';
import { useState, useRef, useEffect, type DragEvent } from 'react';
import { ChevronRight, ChevronDown, Folder, File, FileCode, FileJson, FileText, FileImage, Loader2 } from 'lucide-react';
import { cn } from '../lib/utils';
import type { FileNode } from '../../shared/types';
@@ -73,6 +73,17 @@ export function FileTreeItem({
const [isDragging, setIsDragging] = useState(false);
const dragImageRef = useRef<HTMLDivElement | null>(null);
// Cleanup drag image on unmount to prevent memory leaks
// This handles cases where component unmounts mid-drag or dragend doesn't fire
useEffect(() => {
return () => {
if (dragImageRef.current && dragImageRef.current.parentNode) {
dragImageRef.current.parentNode.removeChild(dragImageRef.current);
dragImageRef.current = null;
}
};
}, []);
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (node.isDirectory) {
@@ -102,10 +113,18 @@ export function FileTreeItem({
e.dataTransfer.setData('text/plain', `@${node.name}`);
e.dataTransfer.effectAllowed = 'copy';
// Create a custom drag image
// Create a custom drag image using safe DOM manipulation (no innerHTML)
const dragImage = document.createElement('div');
dragImage.className = 'flex items-center gap-2 bg-card border border-primary rounded-md px-3 py-2 shadow-lg text-sm';
dragImage.innerHTML = `<span>${node.isDirectory ? '📁' : '📄'}</span><span>${node.name}</span>`;
const iconSpan = document.createElement('span');
iconSpan.textContent = node.isDirectory ? '📁' : '📄';
const nameSpan = document.createElement('span');
nameSpan.textContent = node.name;
dragImage.appendChild(iconSpan);
dragImage.appendChild(nameSpan);
dragImage.style.position = 'absolute';
dragImage.style.top = '-1000px';
dragImage.style.left = '-1000px';
@@ -26,12 +26,16 @@ export function ClaudeOAuthFlow({ onSuccess, onCancel }: ClaudeOAuthFlowProps) {
// Track if we've already started auth to prevent double-execution
const hasStartedRef = useRef(false);
const listenerSetupRef = useRef(false);
// Track the auto-advance timeout so we can cancel it on unmount/re-render
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Listen for OAuth token detection
useEffect(() => {
if (listenerSetupRef.current) return;
listenerSetupRef.current = true;
// Clear any pending timeout from previous effect run
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current);
successTimeoutRef.current = null;
}
const unsubscribe = window.electronAPI.onTerminalOAuthToken((info) => {
console.warn('[ClaudeOAuth] Token event received:', {
@@ -44,7 +48,9 @@ export function ClaudeOAuthFlow({ onSuccess, onCancel }: ClaudeOAuthFlowProps) {
setEmail(info.email);
setStatus('success');
// Auto-advance after a short delay to show success message
setTimeout(() => {
// Store the timeout ID so cleanup can cancel it if needed
successTimeoutRef.current = setTimeout(() => {
successTimeoutRef.current = null; // Clear ref since timeout fired
onSuccess();
}, 1500);
} else {
@@ -54,8 +60,11 @@ export function ClaudeOAuthFlow({ onSuccess, onCancel }: ClaudeOAuthFlowProps) {
});
return () => {
if (unsubscribe) {
unsubscribe();
unsubscribe?.();
// Clear timeout on cleanup to prevent calling onSuccess after unmount
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current);
successTimeoutRef.current = null;
}
};
}, [onSuccess]);