From 64dafe5ac0f8e883435047e8112dbb2071fae4ba Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:58:59 +0100 Subject: [PATCH] fix(base): prompt unsaved changes when discarding dirty rename --- .../components/property/property-menu.tsx | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/apps/client/src/features/base/components/property/property-menu.tsx b/apps/client/src/features/base/components/property/property-menu.tsx index 53d9c5da..b2e0d218 100644 --- a/apps/client/src/features/base/components/property/property-menu.tsx +++ b/apps/client/src/features/base/components/property/property-menu.tsx @@ -50,9 +50,12 @@ export function PropertyMenuContent({ const renameInputRef = useRef(null); const [optionsDirty, setOptionsDirty] = useState(false); const pendingActionRef = useRef<"back" | "close" | null>(null); + const sourcePanelRef = useRef<"rename" | "options" | null>(null); const [closeRequest] = useAtom(propertyMenuCloseRequestAtom) as unknown as [number]; const closeRequestRef = useRef(closeRequest); + const renameDirty = renameValue !== property.name; + const updatePropertyMutation = useUpdatePropertyMutation(); const deletePropertyMutation = useDeletePropertyMutation(); @@ -70,13 +73,21 @@ export function PropertyMenuContent({ } }, [panel]); - const handleOptionsDirtyChange = useCallback( - (dirty: boolean) => { - setOptionsDirty(dirty); - onDirtyChange?.(dirty); - }, - [onDirtyChange], - ); + const handleOptionsDirtyChange = useCallback((dirty: boolean) => { + setOptionsDirty(dirty); + }, []); + + // Single dirty signal to the outside — reflects whichever panel is + // currently accumulating unsaved work. Keeps rename and options in + // lockstep with the `propertyMenuDirtyAtom` so the grid-container's + // outside-click handler and the header's ESC handler both prompt + // "Unsaved changes" consistently. + useEffect(() => { + const dirty = + (panel === "rename" && renameDirty) || + (panel === "options" && optionsDirty); + onDirtyChange?.(dirty); + }, [panel, renameDirty, optionsDirty, onDirtyChange]); const commitRename = useCallback(() => { const trimmed = renameValue.trim(); @@ -94,6 +105,20 @@ export function PropertyMenuContent({ onClose(); }, [commitRename, onClose]); + const requestClose = useCallback(() => { + if (panel === "rename" && renameDirty) { + sourcePanelRef.current = "rename"; + pendingActionRef.current = "close"; + setPanel("confirmDiscard"); + } else if (panel === "options" && optionsDirty) { + sourcePanelRef.current = "options"; + pendingActionRef.current = "close"; + setPanel("confirmDiscard"); + } else { + onClose(); + } + }, [panel, renameDirty, optionsDirty, onClose]); + const handleRenameKeyDown = useCallback( (e: React.KeyboardEvent) => { e.stopPropagation(); @@ -103,10 +128,10 @@ export function PropertyMenuContent({ } if (e.key === "Escape") { e.preventDefault(); - onClose(); + requestClose(); } }, - [handleRenameAndClose, onClose], + [handleRenameAndClose, requestClose], ); const handleOptionsUpdate = useCallback( @@ -131,6 +156,7 @@ export function PropertyMenuContent({ const handleOptionsBack = useCallback(() => { if (optionsDirty) { + sourcePanelRef.current = "options"; pendingActionRef.current = "back"; setPanel("confirmDiscard"); } else { @@ -138,15 +164,6 @@ export function PropertyMenuContent({ } }, [optionsDirty]); - const requestClose = useCallback(() => { - if (panel === "options" && optionsDirty) { - pendingActionRef.current = "close"; - setPanel("confirmDiscard"); - } else { - onClose(); - } - }, [panel, optionsDirty, onClose]); - useEffect(() => { if (closeRequest !== closeRequestRef.current) { closeRequestRef.current = closeRequest; @@ -158,19 +175,22 @@ export function PropertyMenuContent({ const handleConfirmDiscard = useCallback(() => { setOptionsDirty(false); - onDirtyChange?.(false); + setRenameValue(property.name); const action = pendingActionRef.current; pendingActionRef.current = null; + sourcePanelRef.current = null; if (action === "back") { setPanel("main"); } else { onClose(); } - }, [onClose, onDirtyChange]); + }, [property.name, onClose]); const handleCancelDiscard = useCallback(() => { + const source = sourcePanelRef.current ?? "options"; pendingActionRef.current = null; - setPanel("options"); + sourcePanelRef.current = null; + setPanel(source); }, []); return ( @@ -197,7 +217,7 @@ export function PropertyMenuContent({ /> -