Ask user confirmation before leaving an unsaved form
This commit is contained in:
		@@ -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 (
 | 
			
		||||
    <AuthContextK.Provider value={context}>
 | 
			
		||||
      <Routes>
 | 
			
		||||
  const router = createBrowserRouter(
 | 
			
		||||
    createRoutesFromElements(
 | 
			
		||||
      <>
 | 
			
		||||
        <Route path="delete_account" element={<DeleteAccountRoute />} />
 | 
			
		||||
 | 
			
		||||
        {signedIn ? (
 | 
			
		||||
@@ -80,7 +86,13 @@ export function App(): React.ReactElement {
 | 
			
		||||
            <Route path="*" element={<NotFoundRoute />} />
 | 
			
		||||
          </Route>
 | 
			
		||||
        )}
 | 
			
		||||
      </Routes>
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AuthContextK.Provider value={context}>
 | 
			
		||||
      <RouterProvider router={router} />
 | 
			
		||||
    </AuthContextK.Provider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +25,6 @@ async function init() {
 | 
			
		||||
 | 
			
		||||
    root.render(
 | 
			
		||||
      <React.StrictMode>
 | 
			
		||||
        <BrowserRouter>
 | 
			
		||||
        <DarkThemeProvider>
 | 
			
		||||
          <AlertDialogProvider>
 | 
			
		||||
            <ConfirmDialogProvider>
 | 
			
		||||
@@ -42,7 +41,6 @@ async function init() {
 | 
			
		||||
            </ConfirmDialogProvider>
 | 
			
		||||
          </AlertDialogProvider>
 | 
			
		||||
        </DarkThemeProvider>
 | 
			
		||||
        </BrowserRouter>
 | 
			
		||||
      </React.StrictMode>
 | 
			
		||||
    );
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 (
 | 
			
		||||
    <MemberPage
 | 
			
		||||
      member={Member.New(family.family.family_id)}
 | 
			
		||||
      creating={true}
 | 
			
		||||
      editing={true}
 | 
			
		||||
      onCancel={() => 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 (
 | 
			
		||||
    <div style={{ maxWidth: "2000px", margin: "auto" }}>
 | 
			
		||||
      <ConfirmLeaveWithoutSaveDialog
 | 
			
		||||
        shouldBlock={changed && p.shouldAllowLeaving !== true}
 | 
			
		||||
      />
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ import React from "react";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { FamilyApi } from "../../api/FamilyApi";
 | 
			
		||||
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 { useFamily } from "../../widgets/BaseFamilyRoute";
 | 
			
		||||
import { formatDate } from "../../widgets/TimeWidget";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ import {
 | 
			
		||||
} from "@mui/x-data-grid";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { FamilyApi, FamilyUser } from "../../api/FamilyApi";
 | 
			
		||||
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 { AsyncWidget } from "../../widgets/AsyncWidget";
 | 
			
		||||
import { useUser } from "../../widgets/BaseAuthenticatedPage";
 | 
			
		||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,9 @@ import { Outlet, useLocation, useParams } from "react-router-dom";
 | 
			
		||||
import { Family, FamilyApi } from "../api/FamilyApi";
 | 
			
		||||
import { AsyncWidget } from "./AsyncWidget";
 | 
			
		||||
import { RouterLink } from "./RouterLink";
 | 
			
		||||
import { useSnackbar } from "../context_providers/SnackbarProvider";
 | 
			
		||||
import { useConfirm } from "../context_providers/ConfirmDialogProvider";
 | 
			
		||||
import { useAlert } from "../context_providers/AlertDialogProvider";
 | 
			
		||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
 | 
			
		||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
 | 
			
		||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
 | 
			
		||||
 | 
			
		||||
interface FamilyContext {
 | 
			
		||||
  family: Family;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								geneit_app/src/widgets/ConfirmLeaveWithoutSaveDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								geneit_app/src/widgets/ConfirmLeaveWithoutSaveDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
// https://github.com/remix-run/react-router/blob/main/examples/navigation-blocking/src/app.tsx
 | 
			
		||||
// https://stackoverflow.com/questions/75135147/react-router-dom-v6-useblocker
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogActions,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogContentText,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
import { unstable_useBlocker as useBlocker } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export function ConfirmLeaveWithoutSaveDialog(p: {
 | 
			
		||||
  shouldBlock: boolean;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  let blocker = useBlocker(p.shouldBlock);
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (blocker.state === "blocked" && !p.shouldBlock) {
 | 
			
		||||
      blocker.proceed();
 | 
			
		||||
    }
 | 
			
		||||
  }, [blocker, p.shouldBlock]);
 | 
			
		||||
 | 
			
		||||
  const cancelNavigation = () => {
 | 
			
		||||
    blocker.reset?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const confirmNavigation = () => {
 | 
			
		||||
    blocker.proceed?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      open={blocker.state === "blocked"}
 | 
			
		||||
      onClose={cancelNavigation}
 | 
			
		||||
      aria-labelledby="alert-dialog-title"
 | 
			
		||||
      aria-describedby="alert-dialog-description"
 | 
			
		||||
    >
 | 
			
		||||
      <DialogTitle id="alert-dialog-title">
 | 
			
		||||
        Quitter sans enregistrer ?
 | 
			
		||||
      </DialogTitle>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogContentText id="alert-dialog-description">
 | 
			
		||||
          Voulez-vous vraiment quitter cette page sans enregistrer vos
 | 
			
		||||
          modifications ?
 | 
			
		||||
        </DialogContentText>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
      <DialogActions>
 | 
			
		||||
        <Button onClick={confirmNavigation as any}>Quitter la page</Button>
 | 
			
		||||
        <Button onClick={cancelNavigation as any} autoFocus>
 | 
			
		||||
          Rester sur la page
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogActions>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user