From 5827ea474306af73010282ba06245b1958138713 Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:16:18 -0800 Subject: [PATCH] fix unsaved changes showing when no changes in extensions (#6743) --- .../extensions/modal/ExtensionModal.test.tsx | 156 ++++++++++++++++++ .../extensions/modal/ExtensionModal.tsx | 31 ++-- 2 files changed, 172 insertions(+), 15 deletions(-) diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx index 377bc56b..55abed08 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx @@ -5,6 +5,162 @@ import ExtensionModal from './ExtensionModal'; import { ExtensionFormData } from '../utils'; describe('ExtensionModal', () => { + it('does not show unsaved changes dialog when closing without modifications', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + const initialData: ExtensionFormData = { + name: 'Existing Extension', + description: 'An existing extension', + type: 'stdio', + cmd: 'npx some-mcp-server', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [ + { key: 'API_KEY', value: '••••••••', isEdited: false }, + { key: 'OTHER_VAR', value: '••••••••', isEdited: false }, + ], + headers: [], + }; + + render( + + ); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(cancelButton); + + expect(mockOnClose).toHaveBeenCalled(); + + expect(screen.queryByText('Unsaved Changes')).not.toBeInTheDocument(); + }); + + it('shows unsaved changes dialog when name is modified', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + const initialData: ExtensionFormData = { + name: 'Original Name', + description: 'An existing extension', + type: 'stdio', + cmd: 'npx some-mcp-server', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [], + headers: [], + }; + + render( + + ); + + const nameInput = screen.getByPlaceholderText('Enter extension name...'); + await user.clear(nameInput); + await user.type(nameInput, 'New Name'); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(cancelButton); + + expect(screen.getByText('Unsaved Changes')).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + + it('shows unsaved changes dialog when description is modified', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + const initialData: ExtensionFormData = { + name: 'Test Extension', + description: 'Original description', + type: 'stdio', + cmd: 'npx some-mcp-server', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [], + headers: [], + }; + + render( + + ); + + const descriptionInput = screen.getByPlaceholderText('Optional description...'); + await user.clear(descriptionInput); + await user.type(descriptionInput, 'New description'); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(cancelButton); + + expect(screen.getByText('Unsaved Changes')).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + + it('shows unsaved changes dialog when timeout is modified', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + const initialData: ExtensionFormData = { + name: 'Test Extension', + description: 'An extension', + type: 'stdio', + cmd: 'npx some-mcp-server', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [], + headers: [], + }; + + render( + + ); + + const timeoutInput = screen.getByDisplayValue('300'); + await user.clear(timeoutInput); + await user.type(timeoutInput, '600'); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(cancelButton); + + expect(screen.getByText('Unsaved Changes')).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + it('creates a http_streamable extension', async () => { const user = userEvent.setup(); const mockOnSubmit = vi.fn(); diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 947a6d02..e6cbd4d5 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -47,6 +47,12 @@ export default function ExtensionModal({ // Function to check if form has been modified const hasFormChanges = (): boolean => { + // Check basic fields + const nameChanged = formData.name !== initialData.name; + const descriptionChanged = formData.description !== initialData.description; + const typeChanged = formData.type !== initialData.type; + const timeoutChanged = formData.timeout !== initialData.timeout; + // Check if command/endpoint has changed const commandChanged = (formData.type === 'stdio' && formData.cmd !== initialData.cmd) || @@ -54,35 +60,30 @@ export default function ExtensionModal({ (formData.type === 'streamable_http' && formData.endpoint !== initialData.endpoint); // Check if headers have changed - const headersChanged = formData.headers.some((header) => header.isEdited === true); + const headersEdited = formData.headers.some((header) => header.isEdited === true); + const headersAdded = formData.headers.length > initialData.headers.length; + const headersRemoved = formData.headers.length < initialData.headers.length; // Check if any environment variables have been modified const envVarsChanged = formData.envVars.some((envVar) => envVar.isEdited === true); - - // Check if new env vars have been added const envVarsAdded = formData.envVars.length > initialData.envVars.length; - - // Check if env vars have been removed const envVarsRemoved = formData.envVars.length < initialData.envVars.length; - // Check if any environment variable fields have text entered (even if not marked as edited) - const envVarsHaveText = formData.envVars.some( - (envVar) => - (envVar.key.trim() !== '' || envVar.value.trim() !== '') && - // Don't count placeholder values for existing secrets - envVar.value !== '••••••••' - ); - // Check if there are pending environment variables or headers being typed const hasPendingInput = hasPendingEnvVars || hasPendingHeaders; return ( + nameChanged || + descriptionChanged || + typeChanged || + timeoutChanged || commandChanged || - headersChanged || + headersEdited || + headersAdded || + headersRemoved || envVarsChanged || envVarsAdded || envVarsRemoved || - envVarsHaveText || hasPendingInput ); };