feat: implement bulk update for form questions to enable batch saving

This commit is contained in:
2026-05-30 01:08:43 +07:00 Unverified
parent 96c52e8ad6
commit 114804749c
2 changed files with 77 additions and 35 deletions
+33
View File
@@ -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 {
+44 -35
View File
@@ -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<Question[]>(initialQuestions);
const [uploadingId, setUploadingId] = useState<string | null>(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<Question>) => {
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<Question>) => {
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<HTMLInputElement>) => {
@@ -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
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">Questions</h2>
<Button onClick={handleAddQuestion} className="gap-2">
<Plus className="w-4 h-4" />
Add Question
</Button>
<div className="flex items-center gap-2">
<Button onClick={saveQuestions} disabled={isQuestionsSaving} className="gap-2">
{isQuestionsSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
Save Questions
</Button>
<Button onClick={handleAddQuestion} variant="outline" className="gap-2">
<Plus className="w-4 h-4" />
Add Question
</Button>
</div>
</div>
<div className="space-y-4">