Improve admin form workspace UX

This commit is contained in:
2026-07-27 19:11:27 +07:00 Unverified
parent f988f60504
commit bc856402e9
33 changed files with 2194 additions and 596 deletions
+104
View File
@@ -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 };
}