Hook para preguntar si se quieren guardar los cambios en pantallas de edición

This commit is contained in:
David Arranz 2024-08-11 13:10:10 +02:00
parent 738a30d7fa
commit 86a29ad082
7 changed files with 116 additions and 133 deletions

View File

@ -1,4 +1,4 @@
import { AuthProvider, ThemeProvider } from "@/lib/hooks";
import { AuthProvider, ThemeProvider, UnsavedWarnProvider } from "@/lib/hooks";
import { TooltipProvider } from "@/ui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
@ -26,11 +26,13 @@ function App() {
<AuthProvider authActions={createAxiosAuthActions(import.meta.env.VITE_API_URL)}>
<ThemeProvider defaultTheme='light' storageKey='vite-ui-theme'>
<TooltipProvider delayDuration={0}>
<Suspense fallback={<LoadingOverlay />}>
<Routes />
<UnsavedWarnProvider>
<Suspense fallback={<LoadingOverlay />}>
<Routes />
<ToastContainer />
</Suspense>
<ToastContainer />
</Suspense>
</UnsavedWarnProvider>
</TooltipProvider>
<TailwindIndicator />
<ReactQueryDevtools initialIsOpen={false} />

View File

@ -23,7 +23,7 @@ interface CustomDialogProps {
export const CustomDialog = ({
isOpen,
onCancel: onClose,
onCancel,
onConfirm,
title,
description,
@ -39,7 +39,7 @@ export const CustomDialog = ({
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Link to='#' onClick={onClose}>
<Link to='#' onClick={onCancel}>
{cancelLabel}
</Link>
</AlertDialogCancel>

View File

@ -1,10 +1,9 @@
import { UnsavedChangesNotifier, UnsavedWarnProvider } from "@/lib/hooks";
import { UnsavedWarnProvider } from "@/lib/hooks";
import { PropsWithChildren } from "react";
export const Layout = ({ children }: PropsWithChildren) => (
<UnsavedWarnProvider>
<div className='flex flex-col w-full min-h-screen'>{children}</div>
<UnsavedChangesNotifier />
</UnsavedWarnProvider>
);

View File

@ -1,18 +1,55 @@
import { PropsWithChildren, createContext, useState } from "react";
import { CustomDialog } from "@/components";
import { NullOr } from "@shared/utilities";
import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react";
import { UnsavedChangesNotifierProps } from "./useUnsavedChangesNotifier";
export interface IUnsavedWarnContextState {
warnWhen?: boolean;
setWarnWhen?: (value: boolean) => void;
show: (options: NullOr<UnsavedChangesNotifierProps>) => void;
}
export const UnsavedWarnContext = createContext<IUnsavedWarnContextState>({});
export const UnsavedWarnContext = createContext<NullOr<IUnsavedWarnContextState>>(null);
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
const [warnWhen, setWarnWhen] = useState(false);
const [confirm, setConfirm] = useState<NullOr<UnsavedChangesNotifierProps>>(null);
const [open, toggle] = useState(false);
const show = useCallback(
(confirmOptions: NullOr<UnsavedChangesNotifierProps>) => {
setConfirm(confirmOptions);
toggle(true);
},
[toggle, setConfirm]
);
const onConfirm = () => {
confirm?.onConfirm?.();
toggle(false);
};
const onCancel = () => {
confirm?.onCancel?.();
toggle(false);
};
const value = useMemo(() => ({ show }), [show]);
return (
<UnsavedWarnContext.Provider value={{ warnWhen, setWarnWhen }}>
<UnsavedWarnContext.Provider value={value}>
{children}
<CustomDialog
//type='warning'
onCancel={() => {
console.log("onCancel");
onCancel();
}}
onConfirm={() => onConfirm()}
title={confirm?.title}
description={confirm?.subtitle}
confirmLabel={confirm?.confirmText}
cancelLabel={confirm?.cancelText}
isOpen={open}
/>
</UnsavedWarnContext.Provider>
);
};

View File

@ -1,78 +0,0 @@
/**
* `useBlocker` and `usePrompt` is no longer part of react-router-dom for the routers other than `DataRouter`.
*
* The previous workaround (<v6.4) was to use `block` function in `UNSAFE_NavigationContext` which is now removed.
*
* We're using a workaround from the gist https://gist.github.com/MarksCode/64e438c82b0b2a1161e01c88ca0d0355 with some modifications
* Thanks to @MarksCode(https://github.com/MarksCode) for the workaround.
*/
import React from "react";
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
function useConfirmExit(confirmExit: () => boolean, when = true) {
const { navigator } = React.useContext(NavigationContext);
React.useEffect(() => {
if (!when) {
return;
}
const go = navigator.go;
const push = navigator.push;
navigator.push = (...args: Parameters<typeof push>) => {
const result = confirmExit();
if (result !== false) {
push(...args);
}
};
navigator.go = (...args: Parameters<typeof go>) => {
const result = confirmExit();
if (result !== false) {
go(...args);
}
};
return () => {
navigator.push = push;
navigator.go = go;
};
}, [navigator, confirmExit, when]);
}
export function usePrompt(message: string, when = true, onConfirm?: () => void, legacy = false) {
const warnWhenListener = React.useCallback(
(e: { preventDefault: () => void; returnValue: string }) => {
e.preventDefault();
e.returnValue = message;
return e.returnValue;
},
[message]
);
React.useEffect(() => {
if (when && !legacy) {
window.addEventListener("beforeunload", warnWhenListener);
}
return () => {
window.removeEventListener("beforeunload", warnWhenListener);
};
}, [warnWhenListener, when, legacy]);
const confirmExit = React.useCallback(() => {
const confirm = window.confirm(message);
if (confirm && onConfirm) {
onConfirm();
}
return confirm;
}, [message]);
console.log(when);
useConfirmExit(confirmExit, when);
}

View File

@ -1,44 +1,72 @@
import { CustomDialog } from "@/components";
import { t } from "i18next";
import { useEffect, useMemo } from "react";
import { useLocation } from "react-router-dom";
import { useCallback, useEffect } from "react";
import { useBlocker } from "react-router-dom";
import { useWarnAboutChange } from "./useWarnAboutChange";
type UnsavedChangesNotifierProps = {
message?: string;
export type UnsavedChangesNotifierProps = {
title?: string;
subtitle?: string;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
onCancel?: () => void;
type?: "success" | "error" | "warning" | "info";
};
export const UnsavedChangesNotifier = ({
message = t("unsaved_changes_prompt"),
}: UnsavedChangesNotifierProps) => {
const { pathname } = useLocation();
const { warnWhen, setWarnWhen } = useWarnAboutChange();
export const useUnsavedChangesNotifier = ({
isDirty = false,
title = "Se han detectado cambios",
subtitle = "Atención, hay cambios pendientes de guardar en esta página.\nSi continúa, perderá los cambios.",
confirmText = "Continuar",
cancelText = "No continuar",
onConfirm,
onCancel,
type = "warning",
}: UnsavedChangesNotifierProps & { isDirty?: boolean }) => {
const blocker = useBlocker(isDirty);
const { show } = useWarnAboutChange();
const confirm = useCallback(() => {
if (!isDirty) return Promise.resolve(true);
return new Promise<boolean>((resolve) => {
show({
title,
subtitle,
confirmText,
cancelText,
type,
onConfirm: () => {
resolve(true);
onConfirm?.();
},
onCancel: () => {
resolve(false);
onCancel?.();
},
});
});
}, [cancelText, confirmText, isDirty, onCancel, onConfirm, show, subtitle, title, type]);
useEffect(() => {
return () => setWarnWhen?.(false);
}, [pathname]);
if (blocker.state === "blocked") {
confirm().then((result) => {
if (result) blocker.proceed();
else blocker.reset();
});
}
}, [blocker, confirm]);
const warnMessage = useMemo(() => {
return t(message);
}, [message]);
useEffect(() => {
if (isDirty) {
window.onbeforeunload = () => subtitle;
}
return (
<CustomDialog
cancelLabel='Cancelar'
confirmLabel='Confirmar'
onCancel={() => {}}
title='titulo'
isOpen={warnWhen}
onConfirm={() => {
setWarnWhen?.(false);
}}
description={warnMessage}
/>
);
return () => {
window.onbeforeunload = null;
};
}, [isDirty, subtitle]);
/*usePrompt(warnMessage, warnWhen, () => {
setWarnWhen?.(false);
});
return null;*/
return {
confirm,
};
};

View File

@ -6,10 +6,5 @@ export const useWarnAboutChange = () => {
if (context === null)
throw new Error("useWarnAboutChange must be used within a UnsavedWarnProvider");
const { warnWhen, setWarnWhen } = context;
return {
warnWhen: Boolean(warnWhen),
setWarnWhen: setWarnWhen ?? (() => undefined),
};
return context;
};