feat: add Redis-backed SSE updates
This commit is contained in:
@@ -78,6 +78,14 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
|
||||
const formTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const questionsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const formDirtyRef = useRef(false);
|
||||
const questionsDirtyRef = useRef(false);
|
||||
const formSavingRef = useRef(false);
|
||||
const questionsSavingRef = useRef(false);
|
||||
const formRevisionRef = useRef(0);
|
||||
const questionsRevisionRef = useRef(0);
|
||||
const activeFormSaveRef = useRef(0);
|
||||
const activeQuestionsSaveRef = useRef(0);
|
||||
|
||||
// Refs to always read latest state inside debounced callbacks
|
||||
const formTitleRef = useRef(formTitle);
|
||||
@@ -98,6 +106,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
formTimerRef.current = setTimeout(async () => {
|
||||
const title = formTitleRef.current;
|
||||
if (!title.trim()) return;
|
||||
const revision = formRevisionRef.current;
|
||||
activeFormSaveRef.current = revision;
|
||||
formSavingRef.current = true;
|
||||
setFormSaveStatus("saving");
|
||||
try {
|
||||
await updateForm(form.id, {
|
||||
@@ -105,12 +116,19 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
description: formDescriptionRef.current,
|
||||
isOpen: formIsOpenRef.current,
|
||||
});
|
||||
if (formRevisionRef.current === revision) {
|
||||
formDirtyRef.current = false;
|
||||
}
|
||||
setFormSaveStatus("saved");
|
||||
router.refresh();
|
||||
setTimeout(() => setFormSaveStatus("idle"), 2000);
|
||||
} catch {
|
||||
toast.error("Failed to auto-save form settings.");
|
||||
setFormSaveStatus("idle");
|
||||
} finally {
|
||||
if (activeFormSaveRef.current === revision) {
|
||||
formSavingRef.current = false;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}, [form.id, router]);
|
||||
@@ -121,6 +139,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
questionsTimerRef.current = setTimeout(async () => {
|
||||
const current = questionsRef.current;
|
||||
if (current.length === 0) return;
|
||||
const revision = questionsRevisionRef.current;
|
||||
activeQuestionsSaveRef.current = revision;
|
||||
questionsSavingRef.current = true;
|
||||
setQuestionsSaveStatus("saving");
|
||||
try {
|
||||
const formattedUpdates = current.map(q => ({
|
||||
@@ -129,12 +150,19 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
allowOther: q.allowOther,
|
||||
}));
|
||||
await bulkUpdateQuestions(form.id, formattedUpdates);
|
||||
if (questionsRevisionRef.current === revision) {
|
||||
questionsDirtyRef.current = false;
|
||||
}
|
||||
setQuestionsSaveStatus("saved");
|
||||
router.refresh();
|
||||
setTimeout(() => setQuestionsSaveStatus("idle"), 2000);
|
||||
} catch {
|
||||
toast.error("Failed to auto-save questions.");
|
||||
setQuestionsSaveStatus("idle");
|
||||
} finally {
|
||||
if (activeQuestionsSaveRef.current === revision) {
|
||||
questionsSavingRef.current = false;
|
||||
}
|
||||
}
|
||||
}, 1500);
|
||||
}, [form.id, router]);
|
||||
@@ -148,16 +176,22 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
}, []);
|
||||
|
||||
const handleFormTitleChange = (val: string) => {
|
||||
formDirtyRef.current = true;
|
||||
formRevisionRef.current += 1;
|
||||
setFormTitle(val);
|
||||
autoSaveForm();
|
||||
};
|
||||
|
||||
const handleFormDescriptionChange = (val: string) => {
|
||||
formDirtyRef.current = true;
|
||||
formRevisionRef.current += 1;
|
||||
setFormDescription(val);
|
||||
autoSaveForm();
|
||||
};
|
||||
|
||||
const handleFormIsOpenChange = (val: boolean) => {
|
||||
formDirtyRef.current = true;
|
||||
formRevisionRef.current += 1;
|
||||
setFormIsOpen(val);
|
||||
autoSaveForm();
|
||||
};
|
||||
@@ -179,6 +213,8 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
};
|
||||
|
||||
const handleUpdateQuestion = (id: string, updates: Partial<Question>) => {
|
||||
questionsDirtyRef.current = true;
|
||||
questionsRevisionRef.current += 1;
|
||||
setQuestions((prev) =>
|
||||
prev.map((q) => (q.id === id ? { ...q, ...updates } as Question : q))
|
||||
);
|
||||
@@ -188,6 +224,11 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
const handleDeleteQuestion = async (id: string) => {
|
||||
try {
|
||||
await deleteQuestion(id, form.id);
|
||||
setQuestions((current) => {
|
||||
const next = current.filter((question) => question.id !== id);
|
||||
questionsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
router.refresh();
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to delete question:", err);
|
||||
@@ -208,6 +249,8 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
q.displayOrder = idx;
|
||||
});
|
||||
|
||||
questionsDirtyRef.current = true;
|
||||
questionsRevisionRef.current += 1;
|
||||
setQuestions(newQuestions);
|
||||
autoSaveQuestions();
|
||||
};
|
||||
@@ -287,8 +330,21 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setQuestions(initialQuestions), 0);
|
||||
return () => clearTimeout(timeout);
|
||||
if (formDirtyRef.current || formSavingRef.current) return;
|
||||
|
||||
setFormTitle(form.title);
|
||||
setFormDescription(form.description || "");
|
||||
setFormIsOpen(form.isOpen);
|
||||
formTitleRef.current = form.title;
|
||||
formDescriptionRef.current = form.description || "";
|
||||
formIsOpenRef.current = form.isOpen;
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (questionsDirtyRef.current || questionsSavingRef.current) return;
|
||||
|
||||
setQuestions(initialQuestions);
|
||||
questionsRef.current = initialQuestions;
|
||||
}, [initialQuestions]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user