From 0d26550850731d0f8e7f5be6ca57f1b31746ce34 Mon Sep 17 00:00:00 2001 From: AnnuKumar Date: Wed, 29 Jul 2026 00:41:11 +0530 Subject: [PATCH] feat(plugins): add saved drafts support to plugins panel (#596) --- frontend/src/components/PluginsPanel.jsx | 37 +++++- frontend/src/components/PluginsPanel.test.jsx | 112 ++++++++++++++++++ 2 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/PluginsPanel.test.jsx diff --git a/frontend/src/components/PluginsPanel.jsx b/frontend/src/components/PluginsPanel.jsx index 8ca39523..2426941f 100644 --- a/frontend/src/components/PluginsPanel.jsx +++ b/frontend/src/components/PluginsPanel.jsx @@ -14,7 +14,13 @@ const PLUGIN_ICONS = { export default function PluginsPanel({ sessionId, onClose }) { const [plugins, setPlugins] = useState([]); const [selected, setSelected] = useState(null); - const [input, setInput] = useState(""); + + // Persistent input state initialized from localStorage + const [input, setInput] = useState(() => { + if (!sessionId) return ""; + return localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""; + }); + const [output, setOutput] = useState(""); const [running, setRunning] = useState(false); const [error, setError] = useState(""); @@ -23,13 +29,36 @@ export default function PluginsPanel({ sessionId, onClose }) { getPlugins().then(d => setPlugins(d.plugins || [])).catch(()=>{}); }, []); + // Re-sync input draft whenever sessionId changes + useEffect(() => { + if (!sessionId) return; + setInput(localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""); + }, [sessionId]); + + // Persist plugin draft input to localStorage on edit + useEffect(() => { + if (!sessionId) return; + if (input) { + localStorage.setItem(`localmind_plugin_draft_${sessionId}`, input); + } else { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } + }, [input, sessionId]); + async function run() { if (!selected || !input.trim()) return; setRunning(true); setOutput(""); setError(""); try { const r = await runPlugin({ plugin: selected.id, input, session_id: sessionId }); - if (r.success) setOutput(r.output); - else setError(r.error || "Plugin failed"); + if (r.success) { + setOutput(r.output); + // Clear saved draft from localStorage after successful execution + if (sessionId) { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } + } else { + setError(r.error || "Plugin failed"); + } } catch(e) { setError(e.message); } finally { setRunning(false); } } @@ -80,4 +109,4 @@ export default function PluginsPanel({ sessionId, onClose }) { )} ); -} +} \ No newline at end of file diff --git a/frontend/src/components/PluginsPanel.test.jsx b/frontend/src/components/PluginsPanel.test.jsx new file mode 100644 index 00000000..02be6da4 --- /dev/null +++ b/frontend/src/components/PluginsPanel.test.jsx @@ -0,0 +1,112 @@ +// @vitest-environment jsdom +import React from 'react'; +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; +import PluginsPanel from './PluginsPanel'; +import { getPlugins, runPlugin } from '../utils/api'; + +expect.extend(jestDomMatchers); + +vi.mock('../utils/api', () => ({ + getPlugins: vi.fn(), + runPlugin: vi.fn(), +})); + +vi.mock('./Icons', () => ({ + PlugIcon: () => , + CalculatorIcon: () => , + SummaryIcon: () => , + GlobeIcon: () => , + CodeIcon: () => , + HashIcon: () => , + BracesIcon: () => , + ErrorIcon: () => , +})); + +const mockPlugins = [ + { id: 'calculator', name: 'Calculator', description: 'Evaluates expressions', icon: 'calculator' }, +]; + +beforeEach(() => { + localStorage.clear(); + getPlugins.mockResolvedValue({ plugins: mockPlugins }); +}); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('PluginsPanel Saved Drafts (#596)', () => { + test('restores saved plugin draft from localStorage on render', async () => { + localStorage.setItem('localmind_plugin_draft_session-596', '2 + 2'); + + render(); + + await waitFor(() => { + expect(screen.getByText('Calculator')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Calculator')); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + expect(textarea.value).toBe('2 + 2'); + }); + + test('persists plugin draft to localStorage as user types', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('Calculator')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Calculator')); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + fireEvent.change(textarea, { target: { value: '10 * 5' } }); + + expect(localStorage.getItem('localmind_plugin_draft_session-596')).toBe('10 * 5'); + }); + + test('clears saved plugin draft from localStorage upon successful execution', async () => { + runPlugin.mockResolvedValue({ success: true, output: '50' }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Calculator')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Calculator')); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + fireEvent.change(textarea, { target: { value: '10 * 5' } }); + + const runBtn = screen.getByRole('button', { name: /Run Calculator/i }); + fireEvent.click(runBtn); + + await waitFor(() => { + expect(screen.getByText('50')).toBeInTheDocument(); + }); + + expect(localStorage.getItem('localmind_plugin_draft_session-596')).toBeNull(); + }); + + test('switches plugin draft dynamically when sessionId changes', async () => { + localStorage.setItem('localmind_plugin_draft_session-A', 'Draft A'); + localStorage.setItem('localmind_plugin_draft_session-B', 'Draft B'); + + const { rerender } = render(); + + await waitFor(() => expect(screen.getByText('Calculator')).toBeInTheDocument()); + fireEvent.click(screen.getByText('Calculator')); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + expect(textarea.value).toBe('Draft A'); + + rerender(); + + expect(textarea.value).toBe('Draft B'); + }); +}); \ No newline at end of file