From 55a39b28f200e6c1e50213ae616de7de8f3e3f88 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 27 May 2026 05:40:43 +0000 Subject: [PATCH] Prevent navigate from dirty notes --- .../src/components/editors/NotesEditor.tsx | 9 ++- .../src/components/panels/PanelGroup.tsx | 61 +++++++++++-------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/frontend/src/components/editors/NotesEditor.tsx b/src/frontend/src/components/editors/NotesEditor.tsx index c92a90fc3f..1776d47b3c 100644 --- a/src/frontend/src/components/editors/NotesEditor.tsx +++ b/src/frontend/src/components/editors/NotesEditor.tsx @@ -110,10 +110,12 @@ function NoteInfoHover({ note }: { note: any }) { export default function NotesEditor({ modelType, - modelId + modelId, + setDirtyCallback }: Readonly<{ modelType: ModelType; modelId: number; + setDirtyCallback?: (dirty: boolean) => void; }>) { const api = useApi(); const user = useUserState(); @@ -254,6 +256,11 @@ export default function NotesEditor({ selector: ({ editor: e }) => e?.isActive('table') ?? false }); + // Propagate dirty state up to the panel system for navigation guards + useEffect(() => { + setDirtyCallback?.(isDirty); + }, [isDirty, setDirtyCallback]); + // Sync editor editable state when permissions change. // Pass false for emitUpdate to avoid triggering onUpdate (which sets isDirty). useEffect(() => { diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index c07249f303..f1c11b800a 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -10,6 +10,7 @@ import { Stack, Tabs, Text, + Title, Tooltip, UnstyledButton } from '@mantine/core'; @@ -47,6 +48,7 @@ import type { } from '@lib/types/Panel'; import { t } from '@lingui/core/macro'; import { useDocumentVisibility, useWindowEvent } from '@mantine/hooks'; +import { modals } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import { useShallow } from 'zustand/react/shallow'; import { generateUrl } from '../../functions/urls'; @@ -284,21 +286,15 @@ function BasePanelGroup({ [allPanels] ); - // Callback when the active panel changes - const handlePanelChange = useCallback( + const [isDirty, setIsDirty] = useState(false); + useWindowEvent('beforeunload', (event) => { + if (isDirty) { + event.preventDefault(); + } + }); + + const performPanelChange = useCallback( (targetPanel: string, event?: any) => { - cancelEvent(event); - - // check if we are currently on a dirty panel, if so prompt the user to confirm navigation - if (isDirty) { - const confirm = globalThis.confirm( - t`You have unsaved changes, are you sure you want to navigate away from this panel?` - ); - if (!confirm) { - return; - } - } - if (event && eventModified(event)) { const url = `${location.pathname}/../${targetPanel}`; navigateToLink(url, navigate, event); @@ -308,15 +304,39 @@ function BasePanelGroup({ localState.setLastUsedPanel(pageKey)(targetPanel); - // Optionally call external callback hook if (targetPanel && onPanelChange) { onPanelChange(targetPanel); } - // change dirty state setIsDirty(false); }, - [activePanels, navigate, location, onPanelChange] + [navigate, location, pageKey, onPanelChange] + ); + + // Callback when the active panel changes + const handlePanelChange = useCallback( + (targetPanel: string, event?: any) => { + cancelEvent(event); + + if (isDirty) { + modals.openConfirmModal({ + title: {t`Unsaved Changes`}, + children: ( + <> + + {t`You have unsaved changes. Are you sure you want to leave this panel?`} + + ), + labels: { confirm: t`Leave`, cancel: t`Stay` }, + confirmProps: { color: 'red' }, + onConfirm: () => performPanelChange(targetPanel, event) + }); + return; + } + + performPanelChange(targetPanel, event); + }, + [isDirty, performPanelChange] ); // if the selected panel state changes update the current panel @@ -335,13 +355,6 @@ function BasePanelGroup({ } }, [activePanels, panel]); - const [isDirty, setIsDirty] = useState(false); - useWindowEvent('beforeunload', (event) => { - if (isDirty) { - event.preventDefault(); - } - }); - return (