chore: consolidate platform split checks
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, type NavigateFunction, type NavigateOptions, type To } from "react-router-dom";
|
||||
import Button from "./Button";
|
||||
import Dialog from "./Dialog";
|
||||
import DismissibleAlert from "./DismissibleAlert";
|
||||
@@ -13,6 +13,15 @@ export type UnsavedChangesRegistration = {
|
||||
onDiscard?: () => void;
|
||||
};
|
||||
|
||||
export type UnsavedDraftGuardOptions = {
|
||||
dirty: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
onSave: () => boolean | Promise<boolean>;
|
||||
onDiscard?: () => void;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type UnsavedChangesContextValue = {
|
||||
hasUnsavedChanges: boolean;
|
||||
registerUnsavedChanges: (registration: UnsavedChangesRegistration | null) => () => void;
|
||||
@@ -21,13 +30,15 @@ type UnsavedChangesContextValue = {
|
||||
|
||||
const UnsavedChangesContext = createContext<UnsavedChangesContextValue | null>(null);
|
||||
|
||||
export function UnsavedChangesProvider({ children }: { children: ReactNode }) {
|
||||
export function UnsavedChangesProvider({ children }: {children: ReactNode;}) {
|
||||
const navigate = useNavigate();
|
||||
const [registration, setRegistration] = useState<UnsavedChangesRegistration | null>(null);
|
||||
const [registrations, setRegistrations] = useState<Array<{id: number;registration: UnsavedChangesRegistration;}>>([]);
|
||||
const [pendingAction, setPendingAction] = useState<UnsavedNavigationAction | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
const registrationRef = useRef<UnsavedChangesRegistration | null>(null);
|
||||
const nextRegistrationId = useRef(1);
|
||||
const registration = registrations.length ? registrations[registrations.length - 1].registration : null;
|
||||
|
||||
useEffect(() => {
|
||||
registrationRef.current = registration;
|
||||
@@ -36,9 +47,12 @@ export function UnsavedChangesProvider({ children }: { children: ReactNode }) {
|
||||
const hasUnsavedChanges = Boolean(registration);
|
||||
|
||||
const registerUnsavedChanges = useCallback((next: UnsavedChangesRegistration | null) => {
|
||||
setRegistration(next);
|
||||
if (!next) return () => undefined;
|
||||
const id = nextRegistrationId.current;
|
||||
nextRegistrationId.current += 1;
|
||||
setRegistrations((current) => [...current, { id, registration: next }]);
|
||||
return () => {
|
||||
setRegistration((current) => current === next ? null : current);
|
||||
setRegistrations((current) => current.filter((item) => item.id !== id));
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -60,9 +74,12 @@ export function UnsavedChangesProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
function onBeforeUnload(event: BeforeUnloadEvent) {
|
||||
if (!registrationRef.current) return;
|
||||
const active = registrationRef.current;
|
||||
if (!active) return;
|
||||
const message = active.message || "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b";
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
event.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
@@ -111,7 +128,7 @@ export function UnsavedChangesProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
const ok = await active.onSave();
|
||||
if (!ok) {
|
||||
setSaveError("The changes could not be saved. Please review the page message and try again.");
|
||||
setSaveError("i18n:govoplan-core.the_changes_could_not_be_saved_please_review_the.4615a3c6");
|
||||
return;
|
||||
}
|
||||
proceed(action);
|
||||
@@ -139,30 +156,30 @@ export function UnsavedChangesProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<UnsavedChangesContext.Provider value={value}>
|
||||
{children}
|
||||
{pendingAction && registration && (
|
||||
<Dialog
|
||||
open
|
||||
role="alertdialog"
|
||||
title={registration.title ?? "Unsaved changes"}
|
||||
className="unsaved-changes-dialog"
|
||||
footerClassName="unsaved-changes-actions"
|
||||
closeOnBackdrop={!saving}
|
||||
closeDisabled={saving}
|
||||
onClose={() => setPendingAction(null)}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setPendingAction(null)} disabled={saving}>Cancel</Button>
|
||||
<Button onClick={handleDiscardAndLeave} disabled={saving}>Discard</Button>
|
||||
<Button variant="primary" onClick={() => void handleSaveAndLeave()} disabled={saving}>{saving ? "Saving..." : "Save and leave"}</Button>
|
||||
{pendingAction && registration &&
|
||||
<Dialog
|
||||
open
|
||||
role="alertdialog"
|
||||
title={registration.title ?? "i18n:govoplan-core.unsaved_changes.29267269"}
|
||||
className="unsaved-changes-dialog"
|
||||
footerClassName="unsaved-changes-actions"
|
||||
closeOnBackdrop={!saving}
|
||||
closeDisabled={saving}
|
||||
onClose={() => setPendingAction(null)}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => setPendingAction(null)} disabled={saving}>i18n:govoplan-core.cancel.77dfd213</Button>
|
||||
<Button onClick={handleDiscardAndLeave} disabled={saving}>i18n:govoplan-core.discard.36fff63c</Button>
|
||||
<Button variant="primary" onClick={() => void handleSaveAndLeave()} disabled={saving}>{saving ? "i18n:govoplan-core.saving.ae7e8875" : "i18n:govoplan-core.save_and_leave.0507824a"}</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<p>{registration.message ?? "This page has unsaved changes. Save them before leaving, or discard the changes and continue."}</p>
|
||||
}>
|
||||
|
||||
<p>{registration.message ?? "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"}</p>
|
||||
{saveError && <DismissibleAlert tone="danger" resetKey={saveError}>{saveError}</DismissibleAlert>}
|
||||
</Dialog>
|
||||
)}
|
||||
</UnsavedChangesContext.Provider>
|
||||
);
|
||||
}
|
||||
</UnsavedChangesContext.Provider>);
|
||||
|
||||
}
|
||||
|
||||
const fallbackUnsavedChangesContext: UnsavedChangesContextValue = {
|
||||
@@ -182,3 +199,44 @@ export function useRegisterUnsavedChanges(registration: UnsavedChangesRegistrati
|
||||
return registerUnsavedChanges(registration);
|
||||
}, [registerUnsavedChanges, registration]);
|
||||
}
|
||||
|
||||
export function useUnsavedDraftGuard({
|
||||
dirty,
|
||||
title = "i18n:govoplan-core.unsaved_changes.29267269",
|
||||
message = "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b",
|
||||
onSave,
|
||||
onDiscard,
|
||||
enabled = true
|
||||
}: UnsavedDraftGuardOptions) {
|
||||
const saveRef = useRef(onSave);
|
||||
const discardRef = useRef(onDiscard);
|
||||
|
||||
useEffect(() => {
|
||||
saveRef.current = onSave;
|
||||
discardRef.current = onDiscard;
|
||||
}, [onDiscard, onSave]);
|
||||
|
||||
const registration = useMemo<UnsavedChangesRegistration | null>(() => enabled && dirty ? {
|
||||
title,
|
||||
message,
|
||||
onSave: () => saveRef.current(),
|
||||
onDiscard: () => discardRef.current?.()
|
||||
} : null, [dirty, enabled, message, title]);
|
||||
|
||||
useRegisterUnsavedChanges(registration);
|
||||
}
|
||||
|
||||
export function useGuardedNavigate(): NavigateFunction {
|
||||
const navigate = useNavigate();
|
||||
const { requestNavigation } = useUnsavedChanges();
|
||||
|
||||
return useCallback(((to: To | number, options?: NavigateOptions) => {
|
||||
requestNavigation(() => {
|
||||
if (typeof to === "number") {
|
||||
navigate(to);
|
||||
} else {
|
||||
navigate(to, options);
|
||||
}
|
||||
});
|
||||
}) as NavigateFunction, [navigate, requestNavigation]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user