From 114804749cd8a99508dcbcd158163617b5bf4579 Mon Sep 17 00:00:00 2001 From: Gunshiz Date: Sat, 30 May 2026 01:08:43 +0700 Subject: [PATCH] feat: implement bulk update for form questions to enable batch saving --- app/actions/questions.ts | 33 ++++++++++++++++ app/form/admin/client.tsx | 79 ++++++++++++++++++++++----------------- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/app/actions/questions.ts b/app/actions/questions.ts index cfd4161..2ebc9eb 100644 --- a/app/actions/questions.ts +++ b/app/actions/questions.ts @@ -68,6 +68,39 @@ export async function updateQuestion( revalidatePath("/form/admin"); } +export async function bulkUpdateQuestions( + formId: string, + updates: { + id: string; + label: string; + type: QuestionType; + required: boolean; + displayOrder: number; + options: string[]; + }[] +) { + await assertAdmin(); + + // Use Promise.all to update all questions concurrently + await Promise.all( + updates.map((u) => + db + .update(questions) + .set({ + label: u.label, + type: u.type, + required: u.required, + displayOrder: u.displayOrder, + options: u.options ?? [], + }) + .where(eq(questions.id, u.id)) + ) + ); + + revalidatePath("/form"); + revalidatePath("/form/admin"); +} + async function deleteImageFile(imageUrl: string | null) { if (!imageUrl) return; try { diff --git a/app/form/admin/client.tsx b/app/form/admin/client.tsx index 2f55da2..78a65af 100644 --- a/app/form/admin/client.tsx +++ b/app/form/admin/client.tsx @@ -17,6 +17,7 @@ import { updateQuestion, deleteQuestion, reorderQuestions, + bulkUpdateQuestions, } from "@/app/actions/questions"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react"; import Image from 'next/image'; @@ -50,6 +51,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC const [questions, setQuestions] = useState(initialQuestions); const [uploadingId, setUploadingId] = useState(null); const [isFormSaving, startFormSaveTransition] = useTransition(); + const [isQuestionsSaving, startQuestionsSaveTransition] = useTransition(); const saveFormMetadata = () => { if (!formTitle.trim()) { @@ -88,22 +90,26 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC } }; - const handleUpdateQuestion = async (id: string, updates: Partial) => { - try { - setQuestions((prev) => - prev.map((q) => (q.id === id ? { ...q, ...updates } as Question : q)) - ); - - const formattedUpdates = { - ...updates, - options: updates.options === null ? [] : updates.options, - }; - - await updateQuestion(id, form.id, formattedUpdates); - router.refresh(); - } catch (err: any) { - toast.error(err.message || "Failed to update question."); - } + const handleUpdateQuestion = (id: string, updates: Partial) => { + setQuestions((prev) => + prev.map((q) => (q.id === id ? { ...q, ...updates } as Question : q)) + ); + }; + + const saveQuestions = () => { + startQuestionsSaveTransition(async () => { + try { + const formattedUpdates = questions.map(q => ({ + ...q, + options: q.options === null ? [] : q.options, + })); + await bulkUpdateQuestions(form.id, formattedUpdates); + toast.success("Questions saved!"); + router.refresh(); + } catch (err: any) { + toast.error(err.message || "Failed to save questions."); + } + }); }; const handleDeleteQuestion = async (id: string) => { @@ -115,7 +121,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC } }; - const handleMove = async (index: number, direction: "up" | "down") => { + const handleMove = (index: number, direction: "up" | "down") => { const newQuestions = [...questions]; const targetIndex = direction === "up" ? index - 1 : index + 1; if (targetIndex < 0 || targetIndex >= newQuestions.length) return; @@ -124,15 +130,12 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC newQuestions[index] = newQuestions[targetIndex]; newQuestions[targetIndex] = temp; - setQuestions(newQuestions); + // Update display orders based on new positions + newQuestions.forEach((q, idx) => { + q.displayOrder = idx; + }); - try { - await reorderQuestions(form.id, newQuestions.map((q) => q.id)); - toast.success("Order updated!"); - router.refresh(); - } catch (err: any) { - toast.error(err.message || "Failed to reorder questions."); - } + setQuestions(newQuestions); }; const handleImageUpload = async (questionId: string, e: React.ChangeEvent) => { @@ -190,23 +193,23 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC ); }; - const handleAddOption = async (question: Question) => { + const handleAddOption = (question: Question) => { const options = question.options ? [...question.options] : []; options.push(`Option ${options.length + 1}`); - await handleUpdateQuestion(question.id, { options }); + handleUpdateQuestion(question.id, { options }); }; - const handleUpdateOption = async (question: Question, optIndex: number, val: string) => { + const handleUpdateOption = (question: Question, optIndex: number, val: string) => { if (!question.options) return; const options = [...question.options]; options[optIndex] = val; - await handleUpdateQuestion(question.id, { options }); + handleUpdateQuestion(question.id, { options }); }; - const handleRemoveOption = async (question: Question, optIndex: number) => { + const handleRemoveOption = (question: Question, optIndex: number) => { if (!question.options) return; const options = question.options.filter((_, idx) => idx !== optIndex); - await handleUpdateQuestion(question.id, { options }); + handleUpdateQuestion(question.id, { options }); }; React.useEffect(() => { @@ -263,10 +266,16 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC

Questions

- +
+ + +