Improve admin form workspace UX
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { registerPendingSave } from "@/lib/admin-form-save-bridge";
|
||||
|
||||
export type AutoSaveStatus = "clean" | "dirty" | "saving" | "saved" | "error";
|
||||
|
||||
export function useDebouncedAutoSave({
|
||||
save,
|
||||
delay = 1000,
|
||||
}: {
|
||||
save: () => Promise<void>;
|
||||
delay?: number;
|
||||
}) {
|
||||
const [status, setStatus] = useState<AutoSaveStatus>("clean");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const revisionRef = useRef(0);
|
||||
const savedRevisionRef = useRef(0);
|
||||
const savingRef = useRef<Promise<void> | null>(null);
|
||||
const saveRef = useRef(save);
|
||||
|
||||
useEffect(() => {
|
||||
saveRef.current = save;
|
||||
}, [save]);
|
||||
|
||||
const runSave = useCallback(async () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
if (savingRef.current) {
|
||||
await savingRef.current;
|
||||
if (revisionRef.current === savedRevisionRef.current) return;
|
||||
}
|
||||
|
||||
while (revisionRef.current !== savedRevisionRef.current) {
|
||||
const revision = revisionRef.current;
|
||||
setStatus("saving");
|
||||
setError(null);
|
||||
|
||||
const request = saveRef.current()
|
||||
.then(() => {
|
||||
savedRevisionRef.current = revision;
|
||||
setStatus(revisionRef.current === revision ? "saved" : "dirty");
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
setError(reason instanceof Error ? reason.message : "Could not save changes.");
|
||||
setStatus("error");
|
||||
throw reason;
|
||||
})
|
||||
.finally(() => {
|
||||
savingRef.current = null;
|
||||
});
|
||||
|
||||
savingRef.current = request;
|
||||
await request;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const schedule = useCallback(() => {
|
||||
revisionRef.current += 1;
|
||||
setStatus("dirty");
|
||||
setError(null);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => {
|
||||
void runSave().catch(() => undefined);
|
||||
}, delay);
|
||||
}, [delay, runSave]);
|
||||
|
||||
const flush = useCallback(async () => {
|
||||
if (
|
||||
revisionRef.current === savedRevisionRef.current &&
|
||||
!savingRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await runSave();
|
||||
}, [runSave]);
|
||||
|
||||
useEffect(() => registerPendingSave(flush), [flush]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (
|
||||
revisionRef.current !== savedRevisionRef.current ||
|
||||
savingRef.current
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (revisionRef.current !== savedRevisionRef.current) {
|
||||
void saveRef.current().catch(() => undefined);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { status, error, schedule, flush, retry: runSave };
|
||||
}
|
||||
Reference in New Issue
Block a user