From df3b7a8cb0af28495278295e4f996f00f29ed184 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Mon, 16 Mar 2026 01:01:33 +0100 Subject: [PATCH] fix: show validation error when label is empty in NodeModal Silent return when label was empty made the Add button appear broken. Native browser required tooltip doesn't render in Radix Dialog portals. Now shows inline red error message and highlights the field. --- frontend/src/components/modals/NodeModal.tsx | 13 ++-- .../modals/__tests__/NodeModal.test.tsx | 65 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/modals/__tests__/NodeModal.test.tsx diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index 4e5e776..746fd01 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -43,13 +43,18 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' const [form, setForm] = useState>({ ...DEFAULT_DATA, ...initial }) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) + const [labelError, setLabelError] = useState(false) const set = (key: keyof NodeData, value: unknown) => setForm((f) => ({ ...f, [key]: value })) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (!form.label?.trim()) return + if (!form.label?.trim()) { + setLabelError(true) + return + } + setLabelError(false) onSubmit(form) onClose() } @@ -167,11 +172,11 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' set('label', e.target.value)} + onChange={(e) => { set('label', e.target.value); if (labelError) setLabelError(false) }} placeholder="My Server" - className="bg-[#21262d] border-[#30363d] text-sm h-8" - required + className={`bg-[#21262d] text-sm h-8 ${labelError ? 'border-[#f85149] focus-visible:ring-[#f85149]' : 'border-[#30363d]'}`} /> + {labelError &&

Label is required

} {/* Hostname */} diff --git a/frontend/src/components/modals/__tests__/NodeModal.test.tsx b/frontend/src/components/modals/__tests__/NodeModal.test.tsx new file mode 100644 index 0000000..c45a498 --- /dev/null +++ b/frontend/src/components/modals/__tests__/NodeModal.test.tsx @@ -0,0 +1,65 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { NodeModal } from '../NodeModal' + +describe('NodeModal', () => { + it('renders nothing when closed', () => { + const { container } = render( + + ) + expect(container.querySelector('[role="dialog"]')).toBeNull() + }) + + it('renders form fields when open', () => { + render() + expect(screen.getByPlaceholderText('My Server')).toBeDefined() + expect(screen.getByText('Add Node')).toBeDefined() + }) + + it('does not call onSubmit when label is empty and shows error', () => { + const onSubmit = vi.fn() + render() + fireEvent.click(screen.getByText('Add')) + expect(onSubmit).not.toHaveBeenCalled() + expect(screen.getByText('Label is required')).toBeDefined() + }) + + it('calls onSubmit with form data when label is filled', () => { + const onSubmit = vi.fn() + const onClose = vi.fn() + render() + fireEvent.change(screen.getByPlaceholderText('My Server'), { target: { value: 'My NAS' } }) + fireEvent.click(screen.getByText('Add')) + expect(onSubmit).toHaveBeenCalledOnce() + expect(onSubmit.mock.calls[0][0].label).toBe('My NAS') + expect(onClose).toHaveBeenCalledOnce() + }) + + it('clears label error when user starts typing', () => { + render() + fireEvent.click(screen.getByText('Add')) + expect(screen.getByText('Label is required')).toBeDefined() + fireEvent.change(screen.getByPlaceholderText('My Server'), { target: { value: 'x' } }) + expect(screen.queryByText('Label is required')).toBeNull() + }) + + it('pre-fills form from initial prop', () => { + render( + + ) + const input = screen.getByPlaceholderText('My Server') as HTMLInputElement + expect(input.value).toBe('Pre-filled') + }) + + it('shows Save button text when title is Edit Node', () => { + render() + expect(screen.getByText('Save')).toBeDefined() + }) + + it('calls onClose when Cancel is clicked', () => { + const onClose = vi.fn() + render() + fireEvent.click(screen.getByText('Cancel')) + expect(onClose).toHaveBeenCalledOnce() + }) +})