mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { create } from 'zustand';
|
|
|
|
interface AdminUnsavedState {
|
|
/** Sections of the admin panel that hold edits nobody has saved yet. */
|
|
dirty: Record<string, true>;
|
|
/** What to run once the admin chooses "Leave" in the prompt. */
|
|
pendingLeave: (() => void) | null;
|
|
/** The one section the pending leave discards, or null for all of them. */
|
|
pendingScope: string | null;
|
|
setDirty: (id: string, dirty: boolean) => void;
|
|
/**
|
|
* Runs `action` now, or asks first when something is unsaved (#1255).
|
|
* With `scope`, only that section's edits count, for leaving a nested
|
|
* panel while the section around it stays mounted.
|
|
*/
|
|
requestLeave: (action: () => void, scope?: string) => void;
|
|
confirmLeave: () => void;
|
|
cancelLeave: () => void;
|
|
}
|
|
|
|
export const useAdminUnsavedStore = create<AdminUnsavedState>()((set, get) => ({
|
|
dirty: {},
|
|
pendingLeave: null,
|
|
pendingScope: null,
|
|
setDirty: (id, dirty) => {
|
|
const current = get().dirty;
|
|
if (dirty === (id in current)) return;
|
|
const next = { ...current };
|
|
if (dirty) next[id] = true;
|
|
else delete next[id];
|
|
set({ dirty: next });
|
|
},
|
|
requestLeave: (action, scope) => {
|
|
const dirty = get().dirty;
|
|
if (scope ? !(scope in dirty) : Object.keys(dirty).length === 0) {
|
|
action();
|
|
return;
|
|
}
|
|
set({ pendingLeave: action, pendingScope: scope ?? null });
|
|
},
|
|
confirmLeave: () => {
|
|
const { pendingLeave: action, pendingScope: scope, dirty } = get();
|
|
// Clear first: the action unmounts the dirty section, and a hard
|
|
// navigation must not trip the beforeunload prompt a second time.
|
|
let next: Record<string, true> = {};
|
|
if (scope) {
|
|
next = { ...dirty };
|
|
delete next[scope];
|
|
}
|
|
set({ pendingLeave: null, pendingScope: null, dirty: next });
|
|
action?.();
|
|
},
|
|
cancelLeave: () => set({ pendingLeave: null, pendingScope: null }),
|
|
}));
|
|
|
|
export const selectHasUnsaved = (s: AdminUnsavedState) => Object.keys(s.dirty).length > 0;
|
|
|
|
/** Reports a section's unsaved edits to the admin layout's leave prompt. */
|
|
export function useAdminUnsavedChanges(id: string, dirty: boolean) {
|
|
const setDirty = useAdminUnsavedStore((s) => s.setDirty);
|
|
useEffect(() => {
|
|
setDirty(id, dirty);
|
|
}, [id, dirty, setDirty]);
|
|
useEffect(() => () => setDirty(id, false), [id, setDirty]);
|
|
}
|