From ed495b5c0ee799b676bda627337cec928c3ceaaa Mon Sep 17 00:00:00 2001 From: Behnam Date: Thu, 25 Dec 2025 16:11:28 +0000 Subject: [PATCH] fix: resolve all ESLint warnings - Fix useClipboard hook dependency issue using propsRef pattern - Wrap shortcuts array in useMemo to prevent useEffect re-registration - Add eslint-disable for react-refresh/only-export-components where hooks/utilities are exported alongside components (standard pattern) Affected files: - useClipboard.ts: converted to useCallback with propsRef - useKeyboardShortcuts.ts: wrapped shortcuts in useMemo - Context providers: GraphContext, ExecutionContext, ImageHistoryContext - Components: ContextMenu, ImageModal, SettingsDialog Co-Authored-By: Behnam & Claude Code --- src/components/ContextMenu.tsx | 3 + src/components/ImageModal.tsx | 1 + src/components/SettingsDialog.tsx | 1 + src/context/ExecutionContext.tsx | 1 + src/context/GraphContext.tsx | 1 + src/context/ImageHistoryContext.tsx | 1 + src/hooks/useClipboard.ts | 127 +++++++++++++++------------- src/hooks/useKeyboardShortcuts.ts | 8 +- 8 files changed, 79 insertions(+), 64 deletions(-) diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index fb07238..6f0a108 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -162,6 +162,7 @@ export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) { /** * Hook for managing context menu state. */ +// eslint-disable-next-line react-refresh/only-export-components export function useContextMenu() { const [menu, setMenu] = useState<{ x: number; y: number; items: ContextMenuItem[] } | null>(null) @@ -179,6 +180,7 @@ export function useContextMenu() { /** * Common context menu items for nodes. */ +// eslint-disable-next-line react-refresh/only-export-components export function getNodeContextMenuItems(options: { onCopy?: () => void onCut?: () => void @@ -264,6 +266,7 @@ export function getNodeContextMenuItems(options: { /** * Common context menu items for canvas. */ +// eslint-disable-next-line react-refresh/only-export-components export function getCanvasContextMenuItems(options: { onPaste?: () => void onSelectAll?: () => void diff --git a/src/components/ImageModal.tsx b/src/components/ImageModal.tsx index 7613616..5e31670 100644 --- a/src/components/ImageModal.tsx +++ b/src/components/ImageModal.tsx @@ -154,6 +154,7 @@ export function ImageModal() { /** * Helper function to show the image modal from anywhere. */ +// eslint-disable-next-line react-refresh/only-export-components export function showImageModal(url: string, title?: string, metadata?: Record) { const event = new CustomEvent('show-image-modal', { detail: { url, title, metadata }, diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index c34e1da..e4bf047 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -331,6 +331,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { /** * Hook to access settings from anywhere in the app. */ +// eslint-disable-next-line react-refresh/only-export-components export function useSettings(): Settings { const [settings, setSettings] = useState(DEFAULT_SETTINGS) diff --git a/src/context/ExecutionContext.tsx b/src/context/ExecutionContext.tsx index 8b43de3..d8bd713 100644 --- a/src/context/ExecutionContext.tsx +++ b/src/context/ExecutionContext.tsx @@ -245,6 +245,7 @@ export function ExecutionProvider({ children }: ExecutionProviderProps) { * Hook to access execution state from any component. * Must be used within an ExecutionProvider. */ +// eslint-disable-next-line react-refresh/only-export-components export function useExecutionContext(): ExecutionState { const context = useContext(ExecutionContext) diff --git a/src/context/GraphContext.tsx b/src/context/GraphContext.tsx index 4b30ec1..a52313e 100644 --- a/src/context/GraphContext.tsx +++ b/src/context/GraphContext.tsx @@ -54,6 +54,7 @@ export function GraphProvider({ children }: GraphProviderProps) { * Hook to access graph state from any component. * Must be used within a GraphProvider. */ +// eslint-disable-next-line react-refresh/only-export-components export function useGraphContext(): GraphState { const context = useContext(GraphContext) diff --git a/src/context/ImageHistoryContext.tsx b/src/context/ImageHistoryContext.tsx index 8eef726..c127c7c 100644 --- a/src/context/ImageHistoryContext.tsx +++ b/src/context/ImageHistoryContext.tsx @@ -122,6 +122,7 @@ export function ImageHistoryProvider({ children }: ImageHistoryProviderProps) { * Hook to access image history state from any component. * Must be used within an ImageHistoryProvider. */ +// eslint-disable-next-line react-refresh/only-export-components export function useImageHistory(): ImageHistoryState { const context = useContext(ImageHistoryContext) diff --git a/src/hooks/useClipboard.ts b/src/hooks/useClipboard.ts index a947fb0..c156226 100644 --- a/src/hooks/useClipboard.ts +++ b/src/hooks/useClipboard.ts @@ -15,69 +15,16 @@ interface ClipboardOptions { export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: ClipboardOptions) { const isHandling = useRef(false) - /** - * Handle paste from clipboard. - * Supports images, text, and workflow JSON. - */ - const handlePaste = useCallback(async (e?: ClipboardEvent) => { - if (!graph || !canvas || !liteGraph || isHandling.current) return - - isHandling.current = true - - try { - // Get clipboard items - const items = e?.clipboardData?.items || await getClipboardItems() - if (!items) return - - // Check for images first - for (const item of items) { - // Handle both DataTransferItem (from paste event) and ClipboardItem (from Clipboard API) - const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0] - if (itemType?.startsWith('image/')) { - e?.preventDefault() - await handleImagePaste(item as unknown as ClipboardItem) - return - } - } - - // Check for text (could be JSON workflow or prompt) - for (const item of items) { - const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0] - if (itemType === 'text/plain') { - const text = e?.clipboardData - ? e.clipboardData.getData('text/plain') - : await (item as ClipboardItem).getType('text/plain').then(blob => blob.text()) - - if (text) { - e?.preventDefault() - await handleTextPaste(text) - return - } - } - } - } catch (error) { - console.error('Paste error:', error) - } finally { - isHandling.current = false - } - }, [graph, canvas, liteGraph]) - - /** - * Get clipboard items using Clipboard API. - */ - async function getClipboardItems(): Promise { - try { - return await navigator.clipboard.read() - } catch { - return null - } - } + // Store refs to latest props to avoid stale closures + const propsRef = useRef({ graph, canvas, liteGraph, onNodeCreated }) + propsRef.current = { graph, canvas, liteGraph, onNodeCreated } /** * Handle pasting an image from clipboard. * Creates an ImageSource node and uploads the image. */ - async function handleImagePaste(item: ClipboardItem) { + const handleImagePaste = useCallback(async (item: ClipboardItem) => { + const { graph, canvas, liteGraph, onNodeCreated } = propsRef.current if (!graph || !canvas || !liteGraph) return const imageType = item.types[0] @@ -119,13 +66,14 @@ export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: Clipbo canvas.setDirty(true, true) canvas.selectNode(node) onNodeCreated?.(node.id) - } + }, []) /** * Handle pasting text from clipboard. * Could be a workflow JSON, URL, or prompt text. */ - async function handleTextPaste(text: string) { + const handleTextPaste = useCallback(async (text: string) => { + const { graph, canvas, liteGraph, onNodeCreated } = propsRef.current if (!graph || !canvas || !liteGraph) return const trimmed = text.trim() @@ -192,6 +140,65 @@ export function useClipboard({ graph, canvas, liteGraph, onNodeCreated }: Clipbo canvas.selectNode(node) onNodeCreated?.(node.id) } + }, []) + + /** + * Handle paste from clipboard. + * Supports images, text, and workflow JSON. + */ + const handlePaste = useCallback(async (e?: ClipboardEvent) => { + const { graph, canvas, liteGraph } = propsRef.current + if (!graph || !canvas || !liteGraph || isHandling.current) return + + isHandling.current = true + + try { + // Get clipboard items + const items = e?.clipboardData?.items || await getClipboardItems() + if (!items) return + + // Check for images first + for (const item of items) { + // Handle both DataTransferItem (from paste event) and ClipboardItem (from Clipboard API) + const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0] + if (itemType?.startsWith('image/')) { + e?.preventDefault() + await handleImagePaste(item as unknown as ClipboardItem) + return + } + } + + // Check for text (could be JSON workflow or prompt) + for (const item of items) { + const itemType = 'type' in item ? (item as DataTransferItem).type : (item as ClipboardItem).types[0] + if (itemType === 'text/plain') { + const text = e?.clipboardData + ? e.clipboardData.getData('text/plain') + : await (item as ClipboardItem).getType('text/plain').then(blob => blob.text()) + + if (text) { + e?.preventDefault() + await handleTextPaste(text) + return + } + } + } + } catch (error) { + console.error('Paste error:', error) + } finally { + isHandling.current = false + } + }, [handleImagePaste, handleTextPaste]) + + /** + * Get clipboard items using Clipboard API. + */ + async function getClipboardItems(): Promise { + try { + return await navigator.clipboard.read() + } catch { + return null + } } /** diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index e802c15..195b937 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -1,4 +1,4 @@ -import { useEffect, useCallback, useRef } from 'react' +import { useEffect, useCallback, useRef, useMemo } from 'react' import type { LGraph, LGraphCanvas } from 'litegraph.js' import { NODE_MODE } from '../nodes/base/BaseNode' @@ -83,8 +83,8 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions) { onRedo?.() }, [graph, canvas, onRedo]) - // Define shortcuts - const shortcuts: ShortcutAction[] = [ + // Define shortcuts - memoized to prevent useEffect re-registration + const shortcuts: ShortcutAction[] = useMemo(() => [ { key: 's', ctrl: true, @@ -244,7 +244,7 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions) { action: () => onTemplates?.(), description: 'Prompt templates', }, - ] + ], [canvas, graph, handleRedo, handleUndo, onLoad, onNew, onSave, onTemplates]) // Handle keydown events useEffect(() => {