diff --git a/src/Geopilot.Frontend/src/app.tsx b/src/Geopilot.Frontend/src/app.tsx
index cf7cb619..76413169 100644
--- a/src/Geopilot.Frontend/src/app.tsx
+++ b/src/Geopilot.Frontend/src/app.tsx
@@ -17,6 +17,7 @@ import { Imprint } from "./pages/footer/imprint.tsx";
import { DeliveryProvider } from "./pages/delivery/deliveryContext.tsx";
import { CircularProgress } from "@mui/material";
import MandateDetail from "./pages/admin/mandateDetail.tsx";
+import { ControlledNavigateProvider } from "./components/controlledNavigate/controlledNavigateProvider.tsx";
export const App: FC = () => {
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
@@ -25,49 +26,51 @@ export const App: FC = () => {
return (
- {
- setIsSubMenuOpen(true);
- }}
- />
-
-
- {isLoading ? (
-
- ) : (
-
-
-
-
- }
- />
- {isAdmin ? (
- <>
- } />
- }>
- } />
- } />
- } />
- } />
- } />
-
- >
- ) : (
- } />
- )}
- } />
- } />
- } />
-
- )}
-
-
-
+
+ {
+ setIsSubMenuOpen(true);
+ }}
+ />
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+ }
+ />
+ {isAdmin ? (
+ <>
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ >
+ ) : (
+ } />
+ )}
+ } />
+ } />
+ } />
+
+ )}
+
+
+
+
);
diff --git a/src/Geopilot.Frontend/src/components/controlledNavigate/controlledNavigateProvider.tsx b/src/Geopilot.Frontend/src/components/controlledNavigate/controlledNavigateProvider.tsx
new file mode 100644
index 00000000..7aa1320f
--- /dev/null
+++ b/src/Geopilot.Frontend/src/components/controlledNavigate/controlledNavigateProvider.tsx
@@ -0,0 +1,65 @@
+import { createContext, FC, PropsWithChildren, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+export const ControlledNavigateContext = createContext({
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ navigateTo: (path: string) => {},
+ checkIsDirty: false,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ registerCheckIsDirty: (path: string) => {},
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ unregisterCheckIsDirty: (path: string) => {},
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ leaveEditingPage: (canLeave: boolean) => {},
+});
+
+export const ControlledNavigateProvider: FC = ({ children }) => {
+ const [path, setPath] = useState();
+ const [registeredEditPages, setRegisteredEditPages] = useState([]);
+ const [checkIsDirty, setCheckIsDirty] = useState(false);
+ const navigate = useNavigate();
+
+ const registerCheckIsDirty = (path: string) => {
+ if (!registeredEditPages.includes(path)) {
+ setRegisteredEditPages([...registeredEditPages, path]);
+ }
+ };
+
+ const unregisterCheckIsDirty = (path: string) => {
+ setRegisteredEditPages(registeredEditPages.filter(value => value !== path));
+ };
+
+ const navigateTo = (path: string) => {
+ if (
+ registeredEditPages.find(value => {
+ return window.location.pathname.includes(value);
+ })
+ ) {
+ setPath(path);
+ setCheckIsDirty(true);
+ } else {
+ navigate(path);
+ }
+ };
+
+ const leaveEditingPage = (canLeave: boolean) => {
+ if (canLeave && path) {
+ navigate(path);
+ }
+ setCheckIsDirty(false);
+ setPath(undefined);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/Geopilot.Frontend/src/components/controlledNavigate/index.ts b/src/Geopilot.Frontend/src/components/controlledNavigate/index.ts
new file mode 100644
index 00000000..24be9620
--- /dev/null
+++ b/src/Geopilot.Frontend/src/components/controlledNavigate/index.ts
@@ -0,0 +1,4 @@
+import { useContext } from "react";
+import { ControlledNavigateContext } from "./controlledNavigateProvider.tsx";
+
+export const useControlledNavigate = () => useContext(ControlledNavigateContext);
diff --git a/src/Geopilot.Frontend/src/components/header/header.tsx b/src/Geopilot.Frontend/src/components/header/header.tsx
index 0bc9a2f1..29be43a1 100644
--- a/src/Geopilot.Frontend/src/components/header/header.tsx
+++ b/src/Geopilot.Frontend/src/components/header/header.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { FC, useState } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
+import { useLocation } from "react-router-dom";
import {
AppBar,
Avatar,
@@ -25,6 +25,7 @@ import { LanguagePopup } from "./languagePopup";
import MenuIcon from "@mui/icons-material/Menu";
import { FlexRowBox, FlexSpaceBetweenBox } from "../styledComponents.ts";
import { BaseButton } from "../buttons.tsx";
+import { useControlledNavigate } from "../controlledNavigate";
interface HeaderProps {
openSubMenu: () => void;
@@ -32,7 +33,7 @@ interface HeaderProps {
const Header: FC = ({ openSubMenu }) => {
const { t } = useTranslation();
- const navigate = useNavigate();
+ const { navigateTo } = useControlledNavigate();
const location = useLocation();
const { clientSettings } = useAppSettings();
const { user, authEnabled, isAdmin, login, logout } = useGeopilotAuth();
@@ -64,7 +65,7 @@ const Header: FC = ({ openSubMenu }) => {
data-cy="header"
sx={{ padding: "5px 0", cursor: "pointer" }}
onClick={() => {
- navigate("/");
+ navigateTo("/");
}}>
{hasSubMenu ? (
@@ -165,7 +166,7 @@ const Header: FC = ({ openSubMenu }) => {
{
- navigate("/");
+ navigateTo("/");
}}
data-cy="delivery-nav">
@@ -177,7 +178,7 @@ const Header: FC = ({ openSubMenu }) => {
{
- navigate("/admin");
+ navigateTo("/admin");
}}
data-cy="admin-nav">
diff --git a/src/Geopilot.Frontend/src/pages/admin/admin.tsx b/src/Geopilot.Frontend/src/pages/admin/admin.tsx
index c74e3789..e32b24bc 100644
--- a/src/Geopilot.Frontend/src/pages/admin/admin.tsx
+++ b/src/Geopilot.Frontend/src/pages/admin/admin.tsx
@@ -1,9 +1,10 @@
import { useTranslation } from "react-i18next";
import { Box, Divider, Drawer, List, ListItem, ListItemButton, ListItemText, Typography } from "@mui/material";
import { FC } from "react";
-import { Outlet, useNavigate } from "react-router-dom";
+import { Outlet } from "react-router-dom";
import { useAppSettings } from "../../components/appSettings/appSettingsInterface.ts";
import { FlexRowBox } from "../../components/styledComponents.ts";
+import { useControlledNavigate } from "../../components/controlledNavigate";
interface AdminProps {
isSubMenuOpen: boolean;
@@ -12,15 +13,15 @@ interface AdminProps {
const Admin: FC = ({ isSubMenuOpen, setIsSubMenuOpen }) => {
const { t } = useTranslation();
- const navigate = useNavigate();
+ const { navigateTo } = useControlledNavigate();
const { clientSettings } = useAppSettings();
const handleDrawerClose = () => {
setIsSubMenuOpen(false);
};
- const navigateTo = (path: string) => {
- navigate(path);
+ const navigate = (path: string) => {
+ navigateTo(path);
if (isSubMenuOpen) {
handleDrawerClose();
}
@@ -42,7 +43,7 @@ const Admin: FC = ({ isSubMenuOpen, setIsSubMenuOpen }) => {
{
- navigateTo("delivery-overview");
+ navigate("/admin/delivery-overview");
}}
data-cy="admin-delivery-overview-nav">
@@ -56,7 +57,7 @@ const Admin: FC = ({ isSubMenuOpen, setIsSubMenuOpen }) => {
{
- navigateTo(link);
+ navigate("/admin/" + link);
}}
data-cy={`admin-${link}-nav`}>
diff --git a/src/Geopilot.Frontend/src/pages/admin/mandateDetail.tsx b/src/Geopilot.Frontend/src/pages/admin/mandateDetail.tsx
index d6fe5869..4587ad08 100644
--- a/src/Geopilot.Frontend/src/pages/admin/mandateDetail.tsx
+++ b/src/Geopilot.Frontend/src/pages/admin/mandateDetail.tsx
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { PromptContext } from "../../components/prompt/promptContext.tsx";
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
import { ChevronLeft, UndoOutlined } from "@mui/icons-material";
-import { useNavigate, useParams } from "react-router-dom";
+import { useParams } from "react-router-dom";
import {
FormAutocomplete,
FormContainer,
@@ -21,6 +21,7 @@ import { FieldEvaluationType, Mandate, Organisation, ValidationSettings } from "
import { useApi } from "../../api";
import { useGeopilotAuth } from "../../auth";
import { FormAutocompleteValue } from "../../components/form/formAutocomplete.tsx";
+import { useControlledNavigate } from "../../components/controlledNavigate";
export const MandateDetail = () => {
const { t } = useTranslation();
@@ -28,7 +29,8 @@ export const MandateDetail = () => {
const formMethods = useForm({ mode: "all" });
const { showPrompt } = useContext(PromptContext);
const { fetchApi } = useApi();
- const navigate = useNavigate();
+ const { registerCheckIsDirty, unregisterCheckIsDirty, checkIsDirty, leaveEditingPage, navigateTo } =
+ useControlledNavigate();
const { id } = useParams<{
id: string;
}>();
@@ -37,6 +39,41 @@ export const MandateDetail = () => {
const [organisations, setOrganisations] = useState();
const [fileExtensions, setFileExtensions] = useState();
+ useEffect(() => {
+ registerCheckIsDirty(`/admin/mandates/${id}`);
+
+ return () => {
+ unregisterCheckIsDirty(`/admin/mandates/${id}`);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (checkIsDirty) {
+ if (formMethods.formState.isDirty) {
+ showPrompt(t("unsavedChanges"), [
+ { label: t("cancel"), icon: , action: () => leaveEditingPage(false) },
+ {
+ label: t("reset"),
+ icon: ,
+ action: () => leaveEditingPage(true),
+ },
+ {
+ label: t("save"),
+ icon: ,
+ variant: "contained",
+ action: () => {
+ saveMandate(formMethods.getValues() as Mandate, false).then(() => leaveEditingPage(true));
+ },
+ },
+ ]);
+ } else {
+ leaveEditingPage(true);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [checkIsDirty]);
+
const loadMandate = useCallback(() => {
if (id !== "0") {
fetchApi(`/api/v1/mandate/${id}`, { errorMessageLabel: "mandateLoadingError" }).then(setMandate);
@@ -73,7 +110,7 @@ export const MandateDetail = () => {
}
}, [fileExtensions, loadFileExtensions, loadMandate, loadOrganisations, mandate, organisations, user?.isAdmin]);
- async function saveMandate(data: FieldValues, closeAfterSave = false) {
+ async function saveMandate(data: FieldValues, reloadAfterSave = true) {
if (id !== undefined) {
const mandate = data as Mandate;
mandate.deliveries = [];
@@ -88,9 +125,9 @@ export const MandateDetail = () => {
errorMessageLabel: "mandateSaveError",
}).then(response => {
const mandateResponse = response as Mandate;
- if (!closeAfterSave) {
+ if (reloadAfterSave) {
if (id === "0") {
- navigate(`/admin/mandates/${mandateResponse.id}`);
+ navigateTo(`/admin/mandates/${mandateResponse.id}`);
} else {
loadMandate();
}
@@ -99,33 +136,8 @@ export const MandateDetail = () => {
}
}
- const checkChangesBeforeNavigate = () => {
- if (formMethods.formState.isDirty) {
- showPrompt(t("unsavedChanges"), [
- { label: t("cancel"), icon: },
- {
- label: t("reset"),
- icon: ,
- action: () => {
- navigate(`/admin/mandates`);
- },
- },
- {
- label: t("save"),
- icon: ,
- variant: "contained",
- action: () => {
- saveMandate(formMethods.getValues() as Mandate, true).then(() => navigate(`/admin/mandates`));
- },
- },
- ]);
- } else {
- navigate(`/admin/mandates`);
- }
- };
-
const submitForm = (data: FieldValues) => {
- saveMandate(data);
+ saveMandate(data, true);
};
// trigger form validation on mount
@@ -140,7 +152,9 @@ export const MandateDetail = () => {
}
- onClick={checkChangesBeforeNavigate}
+ onClick={() => {
+ navigateTo("/admin/mandates");
+ }}
label={"backToMandates"}
/>
{id !== "0" && {t("id") + ": " + id}}
diff --git a/src/Geopilot.Frontend/src/pages/admin/mandates.tsx b/src/Geopilot.Frontend/src/pages/admin/mandates.tsx
index fc147ed1..cc030185 100644
--- a/src/Geopilot.Frontend/src/pages/admin/mandates.tsx
+++ b/src/Geopilot.Frontend/src/pages/admin/mandates.tsx
@@ -9,12 +9,12 @@ import { Tooltip } from "@mui/material";
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
import AddIcon from "@mui/icons-material/Add";
import { BaseButton } from "../../components/buttons.tsx";
-import { useNavigate } from "react-router-dom";
+import { useControlledNavigate } from "../../components/controlledNavigate";
export const Mandates = () => {
const { t } = useTranslation();
const { user } = useGeopilotAuth();
- const navigate = useNavigate();
+ const { navigateTo } = useControlledNavigate();
const [mandates, setMandates] = useState();
const { fetchApi } = useApi();
@@ -23,7 +23,7 @@ export const Mandates = () => {
}, [fetchApi]);
const startEditing = (id: GridRowId) => {
- navigate(`${id}`);
+ navigateTo(`/admin/mandates/${id}`);
};
useEffect(() => {
diff --git a/src/Geopilot.Frontend/src/pages/footer/footer.tsx b/src/Geopilot.Frontend/src/pages/footer/footer.tsx
index fc3fb638..770a4b9b 100644
--- a/src/Geopilot.Frontend/src/pages/footer/footer.tsx
+++ b/src/Geopilot.Frontend/src/pages/footer/footer.tsx
@@ -1,11 +1,11 @@
import { Button } from "@mui/material";
-import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { FlexRowCenterBox } from "../../components/styledComponents.ts";
+import { useControlledNavigate } from "../../components/controlledNavigate";
const Footer = () => {
const { t } = useTranslation();
- const navigate = useNavigate();
+ const { navigateTo } = useControlledNavigate();
const isAdminRoute = location.pathname.startsWith("/admin");
const marginLeft = isAdminRoute ? "250px" : "0";
@@ -21,28 +21,28 @@ const Footer = () => {