From 11c0cc32e8c308420e232c49009135e75c5f0411 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Tue, 30 May 2023 10:56:27 -0400 Subject: [PATCH 1/2] Reintroduce navigation warning modal --- .../projectSettings/project_settings.ts | 5 ++- src/pages/projectSettings/Context.tsx | 22 +++++++++- src/pages/projectSettings/NavigationModal.tsx | 40 +++++++++++++++++++ src/pages/projectSettings/Tabs.tsx | 2 + 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/pages/projectSettings/NavigationModal.tsx diff --git a/cypress/integration/projectSettings/project_settings.ts b/cypress/integration/projectSettings/project_settings.ts index 5a2b59b2ff..4b03c7d0d9 100644 --- a/cypress/integration/projectSettings/project_settings.ts +++ b/cypress/integration/projectSettings/project_settings.ts @@ -603,9 +603,12 @@ describe( cy.dataCy("display-name-input").should("not.have.attr", "placeholder"); }); - it.skip("Shows a navigation warning modal when navigating away from project settings", () => { + it("Shows a navigation warning modal that lists the general page when navigating away from project settings", () => { cy.contains("My Patches").click(); cy.dataCy("navigation-warning-modal").should("be.visible"); + cy.dataCy("unsaved-pages").within(() => { + cy.get("li").should("have.length", 1); + }); cy.get("body").type("{esc}"); }); diff --git a/src/pages/projectSettings/Context.tsx b/src/pages/projectSettings/Context.tsx index d8fdf51527..7b09b9f1ef 100644 --- a/src/pages/projectSettings/Context.tsx +++ b/src/pages/projectSettings/Context.tsx @@ -203,6 +203,21 @@ const usePopulateForm = ( }, [formData]); // eslint-disable-line react-hooks/exhaustive-deps }; +const useHasUnsavedTab = (): { + hasUnsaved: boolean; + unsavedTabs: ProjectSettingsTabRoutes[]; +} => { + const { tabs } = useProjectSettingsContext(); + const unsavedTabs = Object.entries(tabs) + .filter(([, tabData]) => tabData.hasChanges) + .map(([tab]) => tab as ProjectSettingsTabRoutes); + + return { + unsavedTabs, + hasUnsaved: !!unsavedTabs.length, + }; +}; + const getDefaultTabState = ( defaultValue: T ): Record => @@ -213,4 +228,9 @@ const getDefaultTabState = ( })) ); -export { ProjectSettingsProvider, usePopulateForm, useProjectSettingsContext }; +export { + ProjectSettingsProvider, + useHasUnsavedTab, + usePopulateForm, + useProjectSettingsContext, +}; diff --git a/src/pages/projectSettings/NavigationModal.tsx b/src/pages/projectSettings/NavigationModal.tsx new file mode 100644 index 0000000000..d41e2a95a2 --- /dev/null +++ b/src/pages/projectSettings/NavigationModal.tsx @@ -0,0 +1,40 @@ +import { Body } from "@leafygreen-ui/typography"; +import { matchPath, unstable_useBlocker as useBlocker } from "react-router-dom"; +import { ConfirmationModal } from "components/ConfirmationModal"; +import { routes } from "constants/routes"; +import { useHasUnsavedTab } from "./Context"; +import { getTabTitle } from "./getTabTitle"; + +export const NavigationModal: React.VFC = () => { + const { hasUnsaved, unsavedTabs } = useHasUnsavedTab(); + + const shouldConfirmNavigation = ({ nextLocation }): boolean => { + const isProjectSettingsRoute = + nextLocation && + !!matchPath(`${routes.projectSettings}/*`, nextLocation.pathname); + return !isProjectSettingsRoute && hasUnsaved; + }; + + const blocker = useBlocker(shouldConfirmNavigation); + + return ( + blocker.state === "blocked" && ( + blocker.reset?.()} + onConfirm={() => blocker.proceed?.()} + title="You have unsaved changes that will be discarded. Are you sure you want to leave?" + variant="danger" + > + Unsaved changes are present on the following pages: +
    + {unsavedTabs.map((tab) => ( +
  1. {getTabTitle(tab).title}
  2. + ))} +
+
+ ) + ); +}; diff --git a/src/pages/projectSettings/Tabs.tsx b/src/pages/projectSettings/Tabs.tsx index f150887087..719e363c6f 100644 --- a/src/pages/projectSettings/Tabs.tsx +++ b/src/pages/projectSettings/Tabs.tsx @@ -6,6 +6,7 @@ import { ProjectSettingsQuery, RepoSettingsQuery } from "gql/generated/types"; import { isProduction } from "utils/environmentVariables"; import { useProjectSettingsContext } from "./Context"; import { Header } from "./Header"; +import { NavigationModal } from "./NavigationModal"; import { AccessTab, ContainersTab, @@ -57,6 +58,7 @@ export const ProjectSettingsTabs: React.VFC = ({ return ( +
Date: Thu, 1 Jun 2023 11:01:32 -0400 Subject: [PATCH 2/2] Add handling for project select dropdown --- src/pages/projectSettings/Context.tsx | 10 +++++--- src/pages/projectSettings/NavigationModal.tsx | 25 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/pages/projectSettings/Context.tsx b/src/pages/projectSettings/Context.tsx index 7b09b9f1ef..c43377600f 100644 --- a/src/pages/projectSettings/Context.tsx +++ b/src/pages/projectSettings/Context.tsx @@ -208,9 +208,13 @@ const useHasUnsavedTab = (): { unsavedTabs: ProjectSettingsTabRoutes[]; } => { const { tabs } = useProjectSettingsContext(); - const unsavedTabs = Object.entries(tabs) - .filter(([, tabData]) => tabData.hasChanges) - .map(([tab]) => tab as ProjectSettingsTabRoutes); + const unsavedTabs = useMemo( + () => + Object.entries(tabs) + .filter(([, tabData]) => tabData.hasChanges) + .map(([tab]) => tab as ProjectSettingsTabRoutes), + [tabs] + ); return { unsavedTabs, diff --git a/src/pages/projectSettings/NavigationModal.tsx b/src/pages/projectSettings/NavigationModal.tsx index d41e2a95a2..3480ada29f 100644 --- a/src/pages/projectSettings/NavigationModal.tsx +++ b/src/pages/projectSettings/NavigationModal.tsx @@ -1,18 +1,37 @@ import { Body } from "@leafygreen-ui/typography"; -import { matchPath, unstable_useBlocker as useBlocker } from "react-router-dom"; +import { + matchPath, + unstable_useBlocker as useBlocker, + useParams, +} from "react-router-dom"; import { ConfirmationModal } from "components/ConfirmationModal"; -import { routes } from "constants/routes"; +import { getProjectSettingsRoute, routes } from "constants/routes"; import { useHasUnsavedTab } from "./Context"; import { getTabTitle } from "./getTabTitle"; export const NavigationModal: React.VFC = () => { const { hasUnsaved, unsavedTabs } = useHasUnsavedTab(); + const { projectIdentifier } = useParams(); const shouldConfirmNavigation = ({ nextLocation }): boolean => { const isProjectSettingsRoute = nextLocation && !!matchPath(`${routes.projectSettings}/*`, nextLocation.pathname); - return !isProjectSettingsRoute && hasUnsaved; + if (!isProjectSettingsRoute) { + return hasUnsaved; + } + + /* Identify if the user is navigating to a new project's settings via project select dropdown */ + const currentProjectRoute = getProjectSettingsRoute(projectIdentifier); + const isNewProjectSettingsRoute = !matchPath( + `${currentProjectRoute}/*`, + nextLocation.pathname + ); + if (isNewProjectSettingsRoute) { + return hasUnsaved; + } + + return false; }; const blocker = useBlocker(shouldConfirmNavigation);