From f749b38edcc9b7c8771923a5889cc7ff763c597e Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 26 Jun 2026 16:20:30 +0200 Subject: [PATCH] fix: reset Custom Style draft on reopen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modal is kept mounted by its parent (only `open` toggles), so Radix onOpenChange never fires for a parent-driven open and the draft-reset branch was dead code — abandoned edits leaked into the next open. Reset on the `open` prop edge via effect instead. Also pass radix 10 to parseInt for the size inputs. Adds a reopen-after-cancel regression test. ha-relevant: yes --- .../components/modals/CustomStyleModal.tsx | 24 ++++++++++++------- .../__tests__/CustomStyleModal.test.tsx | 19 +++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/modals/CustomStyleModal.tsx b/frontend/src/components/modals/CustomStyleModal.tsx index 8504d81..c4d962f 100644 --- a/frontend/src/components/modals/CustomStyleModal.tsx +++ b/frontend/src/components/modals/CustomStyleModal.tsx @@ -1,4 +1,4 @@ -import { Fragment, useState, useCallback } from 'react' +import { Fragment, useState, useEffect, useCallback } from 'react' import { toast } from 'sonner' import { Globe, Router, Network, Server, Layers, Box, Container, HardDrive, @@ -160,7 +160,7 @@ function NodeEditor({ nodeType, style, onChange, onApplyToExisting }: NodeEditor min={0} step={10} value={style.width} - onChange={(e) => set('width', parseInt(e.target.value) || 0)} + onChange={(e) => set('width', parseInt(e.target.value, 10) || 0)} className="w-20 h-7 text-xs bg-[#0d1117] border border-[#30363d] rounded px-2 text-[#e6edf3]" /> @@ -171,7 +171,7 @@ function NodeEditor({ nodeType, style, onChange, onApplyToExisting }: NodeEditor min={0} step={10} value={style.height} - onChange={(e) => set('height', parseInt(e.target.value) || 0)} + onChange={(e) => set('height', parseInt(e.target.value, 10) || 0)} className="w-20 h-7 text-xs bg-[#0d1117] border border-[#30363d] rounded px-2 text-[#e6edf3]" /> @@ -285,14 +285,22 @@ export function CustomStyleModal({ open, onClose }: CustomStyleModalProps) { edges: { ...customStyle.edges }, })) - const handleOpen = (isOpen: boolean) => { - if (isOpen) { - // Reset draft to current saved customStyle on open + // Reset the draft to the saved customStyle whenever the modal is (re)opened. + // The parent keeps this component mounted and only toggles `open`, so Radix's + // onOpenChange never fires for a parent-driven open — we key off the prop edge + // instead. Without this, abandoned edits (Cancel) would leak into the next open. + useEffect(() => { + if (open) { setDraft({ nodes: { ...customStyle.nodes }, edges: { ...customStyle.edges } }) setSelection(null) - } else { - onClose() } + // Intentional snapshot-on-open: we don't want live customStyle changes to + // clobber an in-progress edit, only a fresh open should reset. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + + const handleOpen = (isOpen: boolean) => { + if (!isOpen) onClose() } const getNodeStyle = (t: NodeType): NodeTypeStyle => diff --git a/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx b/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx index d368a87..13a4d47 100644 --- a/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx +++ b/frontend/src/components/modals/__tests__/CustomStyleModal.test.tsx @@ -134,4 +134,23 @@ describe('CustomStyleModal', () => { fireEvent.change(widthInputs[0], { target: { value: '250' } }) expect((widthInputs[0] as HTMLInputElement).value).toBe('250') }) + + it('resets abandoned edits when reopened after cancel (mounted parent)', () => { + // Parent keeps the modal mounted and only toggles `open`, so the reset must + // happen on the open-prop edge, not via Radix onOpenChange. + const { rerender } = render() + fireEvent.click(screen.getByRole('button', { name: 'Router' })) + const widthInput = screen.getAllByRole('spinbutton')[0] + fireEvent.change(widthInput, { target: { value: '250' } }) + expect((widthInput as HTMLInputElement).value).toBe('250') + + // Cancel = parent flips open → false, then later → true again. + rerender() + rerender() + + fireEvent.click(screen.getByRole('button', { name: 'Router' })) + const reopenedWidth = screen.getAllByRole('spinbutton')[0] + // Draft was reset to saved style (default width 0) — edit did not leak. + expect((reopenedWidth as HTMLInputElement).value).toBe('0') + }) })