From 1128b5ebd4d844b0c2e67af4524f4a455382b426 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 9 Aug 2023 08:28:37 +0200 Subject: [PATCH] Ask user confirmation before leaving an unsaved form --- geneit_app/src/App.tsx | 22 +++++-- geneit_app/src/dialogs/CreateFamilyDialog.tsx | 2 +- geneit_app/src/dialogs/JoinFamilyDialog.tsx | 2 +- .../context_providers/AlertDialogProvider.tsx | 0 .../ConfirmDialogProvider.tsx | 0 .../context_providers/DarkThemeProvider.tsx | 0 .../context_providers/SnackbarProvider.tsx | 0 geneit_app/src/index.tsx | 42 +++++++------ geneit_app/src/routes/DeleteAccountRoute.tsx | 4 +- geneit_app/src/routes/FamiliesListRoute.tsx | 4 +- geneit_app/src/routes/ProfileRoute.tsx | 4 +- .../src/routes/family/FamilyMemberRoute.tsx | 39 +++++++++--- .../src/routes/family/FamilySettingsRoute.tsx | 4 +- .../routes/family/FamilyUsersListRoute.tsx | 4 +- geneit_app/src/widgets/BaseFamilyRoute.tsx | 6 +- .../widgets/ConfirmLeaveWithoutSaveDialog.tsx | 59 +++++++++++++++++++ geneit_app/src/widgets/DarkThemeButton.tsx | 2 +- 17 files changed, 142 insertions(+), 52 deletions(-) rename geneit_app/src/{ => hooks}/context_providers/AlertDialogProvider.tsx (100%) rename geneit_app/src/{ => hooks}/context_providers/ConfirmDialogProvider.tsx (100%) rename geneit_app/src/{ => hooks}/context_providers/DarkThemeProvider.tsx (100%) rename geneit_app/src/{ => hooks}/context_providers/SnackbarProvider.tsx (100%) create mode 100644 geneit_app/src/widgets/ConfirmLeaveWithoutSaveDialog.tsx diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx index 3f76344..365d4a2 100644 --- a/geneit_app/src/App.tsx +++ b/geneit_app/src/App.tsx @@ -1,5 +1,11 @@ import React from "react"; -import { Route, Routes } from "react-router-dom"; +import { + Route, + RouterProvider, + Routes, + createBrowserRouter, + createRoutesFromElements, +} from "react-router-dom"; import "./App.css"; import { AuthApi } from "./api/AuthApi"; import { DeleteAccountRoute } from "./routes/DeleteAccountRoute"; @@ -41,9 +47,9 @@ export function App(): React.ReactElement { setSignedIn: (s) => setSignedIn(s), }; - return ( - - + const router = createBrowserRouter( + createRoutesFromElements( + <> } /> {signedIn ? ( @@ -80,7 +86,13 @@ export function App(): React.ReactElement { } /> )} - + + ) + ); + + return ( + + ); } diff --git a/geneit_app/src/dialogs/CreateFamilyDialog.tsx b/geneit_app/src/dialogs/CreateFamilyDialog.tsx index 843e6ff..94896bb 100644 --- a/geneit_app/src/dialogs/CreateFamilyDialog.tsx +++ b/geneit_app/src/dialogs/CreateFamilyDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { TextInputDialog } from "./TextInputDialog"; import { ServerApi } from "../api/ServerApi"; import { FamilyApi } from "../api/FamilyApi"; -import { useAlert } from "../context_providers/AlertDialogProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; export function CreateFamilyDialog(p: { open: boolean; diff --git a/geneit_app/src/dialogs/JoinFamilyDialog.tsx b/geneit_app/src/dialogs/JoinFamilyDialog.tsx index 2ec4588..81cc541 100644 --- a/geneit_app/src/dialogs/JoinFamilyDialog.tsx +++ b/geneit_app/src/dialogs/JoinFamilyDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { TextInputDialog } from "./TextInputDialog"; import { ServerApi } from "../api/ServerApi"; import { FamilyApi, JoinFamilyResult } from "../api/FamilyApi"; -import { useAlert } from "../context_providers/AlertDialogProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; export function JoinFamilyDialog(p: { open: boolean; diff --git a/geneit_app/src/context_providers/AlertDialogProvider.tsx b/geneit_app/src/hooks/context_providers/AlertDialogProvider.tsx similarity index 100% rename from geneit_app/src/context_providers/AlertDialogProvider.tsx rename to geneit_app/src/hooks/context_providers/AlertDialogProvider.tsx diff --git a/geneit_app/src/context_providers/ConfirmDialogProvider.tsx b/geneit_app/src/hooks/context_providers/ConfirmDialogProvider.tsx similarity index 100% rename from geneit_app/src/context_providers/ConfirmDialogProvider.tsx rename to geneit_app/src/hooks/context_providers/ConfirmDialogProvider.tsx diff --git a/geneit_app/src/context_providers/DarkThemeProvider.tsx b/geneit_app/src/hooks/context_providers/DarkThemeProvider.tsx similarity index 100% rename from geneit_app/src/context_providers/DarkThemeProvider.tsx rename to geneit_app/src/hooks/context_providers/DarkThemeProvider.tsx diff --git a/geneit_app/src/context_providers/SnackbarProvider.tsx b/geneit_app/src/hooks/context_providers/SnackbarProvider.tsx similarity index 100% rename from geneit_app/src/context_providers/SnackbarProvider.tsx rename to geneit_app/src/hooks/context_providers/SnackbarProvider.tsx diff --git a/geneit_app/src/index.tsx b/geneit_app/src/index.tsx index a6dc414..eeeb293 100644 --- a/geneit_app/src/index.tsx +++ b/geneit_app/src/index.tsx @@ -11,11 +11,11 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; import { BrowserRouter } from "react-router-dom"; -import { ConfirmDialogProvider } from "./context_providers/ConfirmDialogProvider"; -import { AlertDialogProvider } from "./context_providers/AlertDialogProvider"; +import { ConfirmDialogProvider } from "./hooks/context_providers/ConfirmDialogProvider"; +import { AlertDialogProvider } from "./hooks/context_providers/AlertDialogProvider"; import { AsyncWidget } from "./widgets/AsyncWidget"; -import { SnackbarProvider } from "./context_providers/SnackbarProvider"; -import { DarkThemeProvider } from "./context_providers/DarkThemeProvider"; +import { SnackbarProvider } from "./hooks/context_providers/SnackbarProvider"; +import { DarkThemeProvider } from "./hooks/context_providers/DarkThemeProvider"; async function init() { try { @@ -25,24 +25,22 @@ async function init() { root.render( - - - - - -
- await ServerApi.LoadConfig()} - errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !" - build={() => } - /> -
-
-
-
-
-
+ + + + +
+ await ServerApi.LoadConfig()} + errMsg="Echec de la connexion au serveur pour la récupération de la configuration statique !" + build={() => } + /> +
+
+
+
+
); } catch (e) { diff --git a/geneit_app/src/routes/DeleteAccountRoute.tsx b/geneit_app/src/routes/DeleteAccountRoute.tsx index 6555524..7aa65e5 100644 --- a/geneit_app/src/routes/DeleteAccountRoute.tsx +++ b/geneit_app/src/routes/DeleteAccountRoute.tsx @@ -3,9 +3,9 @@ import React from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { AuthApi } from "../api/AuthApi"; import { DeleteAccountTokenInfo, UserApi } from "../api/UserApi"; -import { useAlert } from "../context_providers/AlertDialogProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { AsyncWidget } from "../widgets/AsyncWidget"; -import { useConfirm } from "../context_providers/ConfirmDialogProvider"; +import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; export function DeleteAccountRoute(): React.ReactElement { const alert = useAlert(); diff --git a/geneit_app/src/routes/FamiliesListRoute.tsx b/geneit_app/src/routes/FamiliesListRoute.tsx index ad42e2e..f6076a7 100644 --- a/geneit_app/src/routes/FamiliesListRoute.tsx +++ b/geneit_app/src/routes/FamiliesListRoute.tsx @@ -14,9 +14,9 @@ import React from "react"; import { Family, FamilyApi } from "../api/FamilyApi"; import { CreateFamilyDialog } from "../dialogs/CreateFamilyDialog"; import { JoinFamilyDialog } from "../dialogs/JoinFamilyDialog"; -import { useAlert } from "../context_providers/AlertDialogProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { AsyncWidget } from "../widgets/AsyncWidget"; -import { useConfirm } from "../context_providers/ConfirmDialogProvider"; +import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { RouterLink } from "../widgets/RouterLink"; import { TimeWidget } from "../widgets/TimeWidget"; diff --git a/geneit_app/src/routes/ProfileRoute.tsx b/geneit_app/src/routes/ProfileRoute.tsx index 7de5044..d26798c 100644 --- a/geneit_app/src/routes/ProfileRoute.tsx +++ b/geneit_app/src/routes/ProfileRoute.tsx @@ -13,9 +13,9 @@ import { import React from "react"; import { ServerApi } from "../api/ServerApi"; import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi"; -import { useAlert } from "../context_providers/AlertDialogProvider"; +import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useUser } from "../widgets/BaseAuthenticatedPage"; -import { useConfirm } from "../context_providers/ConfirmDialogProvider"; +import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { PasswordInput } from "../widgets/PasswordInput"; import { formatDate } from "../widgets/TimeWidget"; diff --git a/geneit_app/src/routes/family/FamilyMemberRoute.tsx b/geneit_app/src/routes/family/FamilyMemberRoute.tsx index 40b6467..a78999a 100644 --- a/geneit_app/src/routes/family/FamilyMemberRoute.tsx +++ b/geneit_app/src/routes/family/FamilyMemberRoute.tsx @@ -1,21 +1,22 @@ import ClearIcon from "@mui/icons-material/Clear"; +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; import SaveIcon from "@mui/icons-material/Save"; import { Button, Grid, Stack } from "@mui/material"; import React from "react"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/Delete"; import { useNavigate, useParams } from "react-router-dom"; import { Member, MemberApi } from "../../api/MemberApi"; import { ServerApi } from "../../api/ServerApi"; -import { useAlert } from "../../context_providers/AlertDialogProvider"; -import { useConfirm } from "../../context_providers/ConfirmDialogProvider"; +import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; +import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; +import { AsyncWidget } from "../../widgets/AsyncWidget"; import { useFamily } from "../../widgets/BaseFamilyRoute"; +import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { PropEdit } from "../../widgets/PropEdit"; import { PropertiesBox } from "../../widgets/PropertiesBox"; import { SexSelection } from "../../widgets/SexSelection"; -import { useSnackbar } from "../../context_providers/SnackbarProvider"; -import { AsyncWidget } from "../../widgets/AsyncWidget"; /** * Create a new member route @@ -24,6 +25,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement { const alert = useAlert(); const snackbar = useSnackbar(); + const [shouldQuit, setShouldQuit] = React.useState(false); const n = useNavigate(); const family = useFamily(); @@ -33,6 +35,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement { // TODO : trigger update + setShouldQuit(true); n(family.family.URL(`member/${r.id}`)); snackbar(`La fiche pour ${r.fullName} a été créée avec succès !`); } catch (e) { @@ -41,13 +44,19 @@ export function FamilyCreateMemberRoute(): React.ReactElement { } }; + const cancel = () => { + setShouldQuit(true); + n(family.family.URL("members")); + }; + return ( n(family.family.URL("members"))} + onCancel={cancel} onSave={create} + shouldAllowLeaving={shouldQuit} /> ); } @@ -118,6 +127,8 @@ export function FamilyEditMemberRoute(): React.ReactElement { const alert = useAlert(); const snackbar = useSnackbar(); + const [shouldQuit, setShouldQuit] = React.useState(false); + const family = useFamily(); const { memberId } = useParams(); @@ -126,6 +137,11 @@ export function FamilyEditMemberRoute(): React.ReactElement { setMember(await MemberApi.GetSingle(family.familyId, Number(memberId))); }; + const cancel = () => { + setShouldQuit(true); + n(family.family.URL(`member/${member!.id}`)); + }; + const save = async (m: Member) => { try { await MemberApi.Update(m); @@ -134,6 +150,7 @@ export function FamilyEditMemberRoute(): React.ReactElement { // TODO : update family hook info + setShouldQuit(true); n(family.family.URL(`member/${member!.id}`)); } catch (e) { console.error(e); @@ -151,8 +168,9 @@ export function FamilyEditMemberRoute(): React.ReactElement { member={member!} creating={false} editing={true} - onCancel={() => n(family.family.URL(`member/${member!.id}`))} + onCancel={cancel} onSave={save} + shouldAllowLeaving={shouldQuit} /> )} /> @@ -163,6 +181,7 @@ export function MemberPage(p: { member: Member; editing: boolean; creating: boolean; + shouldAllowLeaving?: boolean; onCancel?: () => void; onSave?: (m: Member) => void; onRequestEdit?: () => void; @@ -170,7 +189,6 @@ export function MemberPage(p: { }): React.ReactElement { const confirm = useConfirm(); - // TODO : add confirmation when leaving page https://dev.to/bangash1996/detecting-user-leaving-page-with-react-router-dom-v602-33ni const [changed, setChanged] = React.useState(false); const [member, setMember] = React.useState(structuredClone(p.member)); @@ -198,6 +216,9 @@ export function MemberPage(p: { return (
+
{ + if (blocker.state === "blocked" && !p.shouldBlock) { + blocker.proceed(); + } + }, [blocker, p.shouldBlock]); + + const cancelNavigation = () => { + blocker.reset?.(); + }; + + const confirmNavigation = () => { + blocker.proceed?.(); + }; + + return ( + + + Quitter sans enregistrer ? + + + + Voulez-vous vraiment quitter cette page sans enregistrer vos + modifications ? + + + + + + + + ); +} diff --git a/geneit_app/src/widgets/DarkThemeButton.tsx b/geneit_app/src/widgets/DarkThemeButton.tsx index c9572c4..cbbcf70 100644 --- a/geneit_app/src/widgets/DarkThemeButton.tsx +++ b/geneit_app/src/widgets/DarkThemeButton.tsx @@ -1,7 +1,7 @@ import Brightness7Icon from "@mui/icons-material/Brightness7"; import DarkModeIcon from "@mui/icons-material/DarkMode"; import { IconButton, Tooltip } from "@mui/material"; -import { useDarkTheme } from "../context_providers/DarkThemeProvider"; +import { useDarkTheme } from "../hooks/context_providers/DarkThemeProvider"; export function DarkThemeButton(): React.ReactElement { const darkTheme = useDarkTheme();