feat(editor) : notify peers of draft overwrite
CI / Verify (push) Successful in 1m20s
CI / Build immutable images and deploy (push) Successful in 1m51s

This commit is contained in:
2026-09-15 19:46:40 +07:00 Unverified
parent fb152c12fe
commit c3a2aa88d6
7 changed files with 121 additions and 50 deletions
+21 -5
View File
@@ -16,6 +16,10 @@ interface SaveResult {
currentVersion?: number;
}
interface SaveIntent {
overwrite?: boolean;
}
export function useAutosave<Value, Draft = Value>({
value,
valid,
@@ -32,7 +36,7 @@ export function useAutosave<Value, Draft = Value>({
value: Value;
valid: boolean;
initialVersion: number;
persist: (value: Value, expectedVersion: number) => Promise<SaveResult>;
persist: (value: Value, expectedVersion: number, intent?: SaveIntent) => Promise<SaveResult>;
delay?: number;
storageKey?: string;
localDraft?: Draft;
@@ -52,6 +56,7 @@ export function useAutosave<Value, Draft = Value>({
const [hydrated, setHydrated] = useState(!storageKey);
const [manualRequest, setManualRequest] = useState(0);
const [conflictVersion, setConflictVersion] = useState<number | null>(null);
const [conflictActor, setConflictActor] = useState<string | null>(null);
const [overwriteRequest, setOverwriteRequest] = useState<number | null>(null);
const handledManualRequest = useRef(0);
const storageWarningShown = useRef(false);
@@ -125,14 +130,15 @@ export function useAutosave<Value, Draft = Value>({
}
}, [draftFingerprint, hydrated, localDraft, savedDraftFingerprint, storageKey, version]);
const save = useEffectEvent(async (snapshot: Value, snapshotFingerprint: string, snapshotDraftFingerprint: string, expectedVersion = version) => {
const save = useEffectEvent(async (snapshot: Value, snapshotFingerprint: string, snapshotDraftFingerprint: string, expectedVersion = version, overwrite = false) => {
setSaving(true);
setErrorFingerprint(null);
try {
const result = await persist(snapshot, expectedVersion);
const result = await persist(snapshot, expectedVersion, overwrite ? { overwrite: true } : undefined);
if (result.status === "conflict") {
if (result.currentVersion) setVersion(result.currentVersion);
setConflictVersion(result.currentVersion ?? expectedVersion);
setConflictActor(null);
return;
}
if (result.status === "error") {
@@ -144,6 +150,7 @@ export function useAutosave<Value, Draft = Value>({
setSavedFingerprint(snapshotFingerprint);
setSavedDraftFingerprint(snapshotDraftFingerprint);
setConflictVersion(null);
setConflictActor(null);
} catch (cause) {
setErrorFingerprint(snapshotFingerprint);
toast.error(cause instanceof Error ? cause.message : "บันทึกอัตโนมัติไม่สำเร็จ");
@@ -162,12 +169,18 @@ export function useAutosave<Value, Draft = Value>({
return;
}
if (hydrated && valid && !saving && fingerprint === savedFingerprint) {
if (event.operation === "overwrite" && event.actorName) {
toast.warning(`${event.actorName} เขียนทับข้อมูลส่วนนี้ด้วย Local draft`, {
description: "ระบบโหลดข้อมูลล่าสุดให้แล้ว",
});
}
setVersion((current) => Math.max(current, event.version));
router.refresh();
return;
}
setVersion((current) => Math.max(current, event.version));
setConflictVersion((current) => Math.max(current ?? 0, event.version));
setConflictActor(event.operation === "overwrite" ? event.actorName ?? null : null);
});
useEffect(() => {
@@ -187,7 +200,9 @@ export function useAutosave<Value, Draft = Value>({
}
toast.error("Autosave ถูกหยุดชั่วคราว", {
id: toastId,
description: "มีผู้แก้ไขคนอื่นบันทึกข้อมูลส่วนนี้ กรุณาเลือกโหลดข้อมูลล่าสุดหรือเขียนทับด้วย Draft นี้",
description: conflictActor
? `${conflictActor} เขียนทับข้อมูลส่วนนี้ แต่ Draft ของคุณยังถูกเก็บไว้`
: "มีผู้แก้ไขคนอื่นบันทึกข้อมูลส่วนนี้ กรุณาเลือกโหลดข้อมูลล่าสุดหรือเขียนทับด้วย Draft นี้",
duration: Infinity,
action: {
label: "ดูตัวเลือก",
@@ -197,7 +212,7 @@ export function useAutosave<Value, Draft = Value>({
return () => {
toast.dismiss(toastId);
};
}, [conflictVersion, guideId, scope]);
}, [conflictActor, conflictVersion, guideId, scope]);
useEffect(() => {
if (!hydrated || !valid || saving || conflictVersion !== null) return;
@@ -218,6 +233,7 @@ export function useAutosave<Value, Draft = Value>({
fingerprint,
draftFingerprint,
overwriteRequest,
true,
).finally(() => setOverwriteRequest(null));
}, 0);
return () => window.clearTimeout(timer);