From 3d2eefc63a699055ed5cd08600af1bc36f770841 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:38:05 +0100 Subject: [PATCH] Close the settings dialog from the live URL, not a stale location --- .../core/components/shared/AppConfigModal.tsx | 31 +++++++++++-------- frontend/editor/src/core/pages/HomePage.tsx | 12 ++++--- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/frontend/editor/src/core/components/shared/AppConfigModal.tsx b/frontend/editor/src/core/components/shared/AppConfigModal.tsx index 12faf6eea5..e01146d5ab 100644 --- a/frontend/editor/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/editor/src/core/components/shared/AppConfigModal.tsx @@ -31,6 +31,7 @@ import { } from "@app/contexts/UnsavedChangesContext"; import { stripBasePath, withBasePath } from "@app/constants/app"; import { EDITOR_BASENAME } from "@app/routes/editorBasename"; +import { isInSettings } from "@app/utils/settingsNavigation"; interface AppConfigModalProps { opened: boolean; @@ -88,6 +89,10 @@ const AppConfigModalInner: React.FC = ({ const licenseAlert = useLicenseAlert(); const { confirmIfDirty } = useUnsavedChanges(); const closeButtonRef = useRef(null); + // Whether this modal pushed its own /settings entry, so close knows there is + // one to pop. `location.key` can't answer that: react-router defers location + // updates through a transition and may still read the pre-open entry. + const pushedSettingsEntry = useRef(false); // Sync active state with URL path. Runs on open, on external URL changes, // and on the redirect path below - NOT on intra-modal tab clicks, because @@ -146,6 +151,7 @@ const AppConfigModalInner: React.FC = ({ withBasePath(`/settings/${key}`), ); } else { + pushedSettingsEntry.current = true; navigate(`/settings/${key}`); } }, @@ -223,24 +229,23 @@ const AppConfigModalInner: React.FC = ({ // Only unwind history if settings was opened via the URL; opened via state // there's no /settings entry to pop and navigate(-1) would jump to /files. - if (urlSync && location.pathname.startsWith("/settings")) { - // "default" key = first entry (deep link/refresh); nothing to pop to. - if (location.key === "default") { - navigate(EDITOR_BASENAME, { replace: true }); - } else { + // Both checks read the live URL, not `location`: tab switches rewrite the + // address bar through `history.replaceState`, and react-router defers its + // own location updates through a transition, so `location` can still hold + // the pre-open path here and skip the unwind entirely - leaving the URL on + // /settings/* so the modal immediately re-opens. + if (urlSync && isInSettings()) { + if (pushedSettingsEntry.current) { + pushedSettingsEntry.current = false; navigate(-1); + } else { + // Deep link or refresh straight into /settings: nothing to pop to. + navigate(EDITOR_BASENAME, { replace: true }); } } onClose(); return true; - }, [ - confirmIfDirty, - location.key, - location.pathname, - navigate, - onClose, - urlSync, - ]); + }, [confirmIfDirty, navigate, onClose, urlSync]); // Synchronous wrapper for contexts (e.g. tour buttons) that need () => void const handleCloseSync = useCallback(() => { diff --git a/frontend/editor/src/core/pages/HomePage.tsx b/frontend/editor/src/core/pages/HomePage.tsx index b1d8708649..b6714c9878 100644 --- a/frontend/editor/src/core/pages/HomePage.tsx +++ b/frontend/editor/src/core/pages/HomePage.tsx @@ -29,6 +29,7 @@ import LocalIcon from "@app/components/shared/LocalIcon"; import AppConfigModal from "@app/components/shared/AppConfigModalLazy"; import { getStartupNavigationAction } from "@app/utils/homePageNavigation"; import { EDITOR_BASENAME } from "@app/routes/editorBasename"; +import { isInSettings } from "@app/utils/settingsNavigation"; import { HomePageExtensions } from "@app/components/home/HomePageExtensions"; import { FilesPageProvider, @@ -103,9 +104,12 @@ export default function HomePage() { // Open the config modal whenever the URL is /settings/* (e.g. from the admin // tour's openConfigModal action which navigates to /settings/overview). + // + // Read the live URL rather than `location.pathname`: react-router defers + // location updates through a transition, so under load it can still hold the + // pre-navigation path and re-open a modal the user just closed. useEffect(() => { - const isSettings = location.pathname.startsWith("/settings"); - setConfigModalOpen(isSettings); + setConfigModalOpen(isInSettings()); }, [location.pathname]); useEffect(() => { @@ -116,10 +120,10 @@ export default function HomePage() { const handleCloseConfig = useCallback(() => { setConfigModalOpen(false); - if (location.pathname.startsWith("/settings")) { + if (isInSettings()) { navigate(EDITOR_BASENAME, { replace: true }); } - }, [location.pathname, navigate]); + }, [navigate]); const { activeFiles } = useFileContext(); const navigationState = useNavigationState();