Skip to content

Commit fa832cf

Browse files
Manage questions rework (#680)
* push question changes to backend + required * useMutation switch to onSettled
1 parent 811c1be commit fa832cf

File tree

1 file changed

+82
-94
lines changed
  • frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/questions

1 file changed

+82
-94
lines changed

frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/questions/campaign-questions.tsx

Lines changed: 82 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"use client";
22

3-
import { cn, dateToString } from "@/lib/utils";
3+
import { cn } from "@/lib/utils";
44
import { getCampaign, getCampaignRoles, RoleDetails } from "@/models/campaign";
5-
import { useQuery, useQueryClient } from "@tanstack/react-query";
6-
import { ArrowLeft, Check, ChevronsUpDown, GripVertical, Plus, Trash, X } from "lucide-react";
5+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6+
import { ArrowLeft, Asterisk, Check, ChevronsUpDown, GripVertical, Plus, Trash, X } from "lucide-react";
77
import Link from "next/link";
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
99
import { createQuestion, deleteQuestion, getAllCommonQuestions, getAllRoleQuestions, MultiOptionQuestionOption, Question, QuestionType, updateQuestion } from "@/models/question";
10-
import { useEffect, useState } from "react";
10+
import { useState } from "react";
1111
import { Input } from "@/components/ui/input";
12-
import { Label } from "@/components/ui/label";
1312
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1413
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
1514
import { snowflakeGenerator } from "@/lib";
@@ -20,7 +19,6 @@ import {
2019
AlertDialogAction,
2120
AlertDialogCancel,
2221
AlertDialogContent,
23-
AlertDialogDescription,
2422
AlertDialogFooter,
2523
AlertDialogHeader,
2624
AlertDialogTitle,
@@ -70,103 +68,62 @@ export default function CampaignQuestions({ campaignId, orgId, dict }: { campaig
7068
}
7169
});
7270

73-
const [allCommonQuestions, setAllCommonQuestions] = useState<Question[]>(commonQuestions ?? []);
74-
const [allRoleQuestions, setAllRoleQuestions] = useState<{ role: RoleDetails, questions: Question[] }[]>(rolesAndQuestions ?? []);
71+
const { mutateAsync: mutateUpdateQuestion } = useMutation({
72+
mutationFn: ({ questionId, question }: { questionId: string; question: Question }) =>
73+
updateQuestion(campaignId, questionId, question),
74+
onSettled: () => {
75+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-common-questions`] });
76+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] });
77+
},
78+
});
7579

76-
useEffect(() => {
77-
if (commonQuestions) {
78-
setAllCommonQuestions(commonQuestions);
79-
}
80-
if (rolesAndQuestions) {
81-
setAllRoleQuestions(rolesAndQuestions);
80+
const { mutateAsync: mutateDeleteQuestion } = useMutation({
81+
mutationFn: (questionId: string) => deleteQuestion(campaignId, questionId),
82+
onSettled: () => {
83+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-common-questions`] });
84+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] });
8285
}
83-
}, [commonQuestions, rolesAndQuestions]);
86+
});
8487

85-
const [changedQuestions, setChangedQuestions] = useState<Question[]>([]);
86-
const [newQuestions, setNewQuestions] = useState<Question[]>([]);
88+
const { mutateAsync: mutateCreateQuestion } = useMutation({
89+
mutationFn: (question: Question) => createQuestion(campaignId, question),
90+
onSettled: () => {
91+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-common-questions`] });
92+
queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] });
93+
},
94+
});
8795

8896
const handleQuestionUpdate = async (action: "update" | "delete", question: Question) => {
8997
if (action === 'delete') {
90-
await deleteQuestion(campaignId, question.id);
91-
await queryClient.invalidateQueries({ queryKey: [`${campaignId}-common-questions`] });
92-
await queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] });
93-
return;
94-
}
95-
96-
97-
if (newQuestions.some((q) => q.id === question.id)) {
98-
setNewQuestions(newQuestions.map((q) => q.id === question.id ? question : q));
99-
} else if (!changedQuestions.some((q) => q.id === question.id)) {
100-
setChangedQuestions([...changedQuestions, question]);
98+
await mutateDeleteQuestion(question.id);
10199
} else {
102-
setChangedQuestions(changedQuestions.map((q) => q.id === question.id ? question : q));
100+
await mutateUpdateQuestion({ questionId: question.id, question });
103101
}
104-
105-
if (question.common) {
106-
setAllCommonQuestions(allCommonQuestions.map((q) => q.id === question.id ? question : q));
107-
} else {
108-
setAllRoleQuestions(allRoleQuestions.map(({ role, questions }) => { return { role, questions: questions.map((q) => q.id === question.id ? question : q) } }));
109-
}
110-
}
111-
112-
const saveQuestions = async () => {
113-
await Promise.all(changedQuestions.map(async (question) => {
114-
await updateQuestion(campaignId, question.id, question);
115-
}))
116-
await Promise.all(newQuestions.map(async (question) => {
117-
await createQuestion(campaignId, question);
118-
}))
119-
120-
setChangedQuestions([]);
121-
setNewQuestions([]);
122-
123-
await queryClient.invalidateQueries({ queryKey: [`${campaignId}-common-questions`] });
124-
await queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] })
125102
}
126103

127-
const addNewQuestion = (type: QuestionType, roleId: string) => {
104+
const addNewQuestion = async (type: QuestionType, roleId: string) => {
128105
const common = roleId === "common";
129106

130-
let newQuestion: Question = { id: snowflakeGenerator.generate().toString(), title: "", description: "", roles: [roleId], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), question_type: type, data: { options: [] }, common, required: false };
107+
let newQuestion: Question = { id: snowflakeGenerator.generate().toString(), title: "", description: "", roles: common ? [] : [roleId], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), question_type: type, data: { options: [{ id: snowflakeGenerator.generate().toString(), display_order: 1, text: "Default Option" }] }, common, required: false };
131108
if (type === 'ShortAnswer') {
132109
delete (newQuestion as any).data;
133110
}
134111

135-
if (common) {
136-
newQuestion.roles = [];
137-
setAllCommonQuestions([...allCommonQuestions, newQuestion]);
138-
} else {
139-
setAllRoleQuestions(allRoleQuestions.map(({ role, questions }) => {
140-
if (role.id === roleId) {
141-
return { role, questions: [...questions, newQuestion] };
142-
}
143-
144-
return { role, questions };
145-
}));
146-
}
147-
148-
setNewQuestions([...newQuestions, newQuestion]);
112+
await mutateCreateQuestion(newQuestion);
149113
}
150114

151-
const addExistingQuestion = (questionId: string, oldRoleId: string, newRoleId: string) => {
115+
const addExistingQuestion = async (questionId: string, oldRoleId: string, newRoleId: string) => {
152116
// Common questions cannot be shared with roles
153117
if (newRoleId === "common" || oldRoleId === "common") { return; }
154118

155-
// Update all instances of the question with the new roleId
156-
// setAllRoleQuestions(allRoleQuestions.map(({ role, questions }) => {
157-
// const updatedQuestions = questions.map((question) => question.id === questionId ? { ...question, roles: [...question.roles, roleId] } : question);
158-
// return {role, questions: updatedQuestions};
159-
// }));
160-
const question = allRoleQuestions.find(({ role }) => role.id === oldRoleId)?.questions.find((question) => question.id === questionId);
119+
const question = rolesAndQuestions
120+
?.find(({ role }) => role.id === oldRoleId)
121+
?.questions.find((question) => question.id === questionId);
122+
161123
if (!question) { return; }
162124

163125
const updatedQuestion = { ...question, roles: [...question.roles, newRoleId] };
164-
const update = async () => {
165-
await updateQuestion(campaignId, question.id, updatedQuestion);
166-
await queryClient.invalidateQueries({ queryKey: [`${campaignId}-all-role-questions`] });
167-
}
168-
169-
update();
126+
await mutateUpdateQuestion({ questionId: question.id, question: updatedQuestion });
170127
}
171128

172129
return (
@@ -185,23 +142,20 @@ export default function CampaignQuestions({ campaignId, orgId, dict }: { campaig
185142
</div>
186143
<div className="mt-2 pb-10">
187144
<Tabs defaultValue="common" className="max-w-[1000px]">
188-
<div className="flex items-center justify-between">
189-
<TabsList>
190-
<TabsTrigger value="common">Common</TabsTrigger>
191-
{roles?.map((role) => (
192-
<TabsTrigger key={role.id} value={role.id}>{role.name}</TabsTrigger>
193-
))}
194-
</TabsList>
195-
<Button disabled={changedQuestions.length === 0 && newQuestions.length === 0} onClick={saveQuestions}>Save</Button>
196-
</div>
145+
<TabsList>
146+
<TabsTrigger value="common">Common</TabsTrigger>
147+
{roles?.map((role) => (
148+
<TabsTrigger key={role.id} value={role.id}>{role.name}</TabsTrigger>
149+
))}
150+
</TabsList>
197151
<TabsContent value="common">
198-
<QuestionEditor campaignId={campaignId} questions={allCommonQuestions} handleQuestionUpdate={handleQuestionUpdate} dict={dict} />
199-
<NewQuestionButton currentRole="common" allRoleQuestions={allRoleQuestions} onAddNew={(type) => addNewQuestion(type, "common")} onAddExisting={(questionId) => { }} disableExisting={true} dict={dict} />
152+
<QuestionEditor questions={commonQuestions ?? []} handleQuestionUpdate={handleQuestionUpdate} dict={dict} />
153+
<NewQuestionButton currentRole="common" allRoleQuestions={rolesAndQuestions ?? []} onAddNew={(type) => addNewQuestion(type, "common")} onAddExisting={(questionId) => { }} disableExisting={true} dict={dict} />
200154
</TabsContent>
201-
{allRoleQuestions?.map(({ role, questions }) => (
155+
{rolesAndQuestions?.map(({ role, questions }) => (
202156
<TabsContent key={role.id} value={role.id}>
203-
<QuestionEditor campaignId={campaignId} possibleRole={role} questions={questions} handleQuestionUpdate={handleQuestionUpdate} dict={dict} />
204-
<NewQuestionButton currentRole={role.id} allRoleQuestions={allRoleQuestions} onAddNew={(type) => addNewQuestion(type, role.id)} onAddExisting={(questionId, oldRoleId) => addExistingQuestion(questionId, oldRoleId, role.id)} dict={dict} />
157+
<QuestionEditor possibleRole={role} questions={questions} handleQuestionUpdate={handleQuestionUpdate} dict={dict} />
158+
<NewQuestionButton currentRole={role.id} allRoleQuestions={rolesAndQuestions ?? []} onAddNew={(type) => addNewQuestion(type, role.id)} onAddExisting={(questionId, oldRoleId) => addExistingQuestion(questionId, oldRoleId, role.id)} dict={dict} />
205159
</TabsContent>
206160
))}
207161
</Tabs>
@@ -331,7 +285,7 @@ function ExistingQuestionsCombobox({ allRoleQuestions, setQuestion, setOldRoleId
331285
)
332286
}
333287

334-
function QuestionEditor({ possibleRole, questions, handleQuestionUpdate, dict }: { campaignId: string, possibleRole?: RoleDetails, questions?: Question[], handleQuestionUpdate: (action: "update" | "delete", question: Question) => Promise<void>, dict: any }) {
288+
function QuestionEditor({ possibleRole, questions, handleQuestionUpdate, dict }: { possibleRole?: RoleDetails, questions?: Question[], handleQuestionUpdate: (action: "update" | "delete", question: Question) => Promise<void>, dict: any }) {
335289
const roleId = possibleRole?.id ?? "common";
336290

337291
return (
@@ -350,6 +304,7 @@ function MultiOptionQuestionCard({ question, currentRole, possibleRole, handleQu
350304
const [title, setTitle] = useState<string>(question?.title ?? "");
351305
const [questionType, setQuestionType] = useState<string>(question?.question_type ?? "");
352306
const [options, setOptions] = useState<MultiOptionQuestionOption[]>(question?.data?.options ?? []);
307+
const [required, setRequired] = useState<boolean>(question?.required ?? false);
353308

354309
const handleDragEnd = async (result: DropResult) => {
355310
if (!result.destination) {
@@ -403,12 +358,28 @@ function MultiOptionQuestionCard({ question, currentRole, possibleRole, handleQu
403358
await handleQuestionUpdate('update', updatedQuestion);
404359
}
405360

361+
const toggleRequired = async () => {
362+
const newRequired = !required;
363+
setRequired(newRequired);
364+
await handleQuestionUpdate('update', { ...question!, required: newRequired });
365+
}
366+
406367
return (
407368
<div className="flex flex-col p-2 border rounded-md gap-2 w">
408369
<div className="flex flex-col gap-1">
409370
<div className="flex justify-between">
410371
<Input className="max-w-[500px]" value={title} onChange={async (e) => await updateTitle(e.target.value)} />
411372
<div className="flex items-center gap-1">
373+
<Tooltip>
374+
<TooltipTrigger asChild>
375+
<Button variant={required ? "default" : "outline"} onClick={toggleRequired}>
376+
<Asterisk className="w-4 h-4" />
377+
</Button>
378+
</TooltipTrigger>
379+
<TooltipContent>
380+
<p>{required ? "Required" : "Optional"}</p>
381+
</TooltipContent>
382+
</Tooltip>
412383
{
413384
question?.roles && question?.roles.length > 1 && (
414385
<Tooltip>
@@ -510,6 +481,7 @@ function OptionDecorator({ questionType, index }: { questionType: string, index:
510481

511482
function ShortAnswerQuestionCard({ question, currentRole, possibleRole, handleQuestionUpdate, dict }: { question?: Question, currentRole: string, possibleRole?: RoleDetails, handleQuestionUpdate: (action: "update" | "delete", question: Question) => Promise<void>, dict: any }) {
512483
const [title, setTitle] = useState(question?.title ?? "");
484+
const [required, setRequired] = useState(question?.required ?? false);
513485

514486
const updateTitle = async (title: string) => {
515487
setTitle(title);
@@ -525,11 +497,27 @@ function ShortAnswerQuestionCard({ question, currentRole, possibleRole, handleQu
525497
await handleQuestionUpdate('update', updatedQuestion);
526498
}
527499

500+
const toggleRequired = async () => {
501+
const newRequired = !required;
502+
setRequired(newRequired);
503+
await handleQuestionUpdate('update', { ...question!, required: newRequired });
504+
}
505+
528506
return (
529507
<div className="flex flex-col justify-between p-2 border rounded-md gap-2 min-h-[120px]">
530508
<div className="flex justify-between">
531509
<Input className="max-w-[500px]" value={title} onChange={async (e) => await updateTitle(e.target.value)} />
532510
<div className="flex items-center gap-1">
511+
<Tooltip>
512+
<TooltipTrigger asChild>
513+
<Button variant={required ? "default" : "outline"} onClick={toggleRequired}>
514+
<Asterisk className="w-4 h-4" />
515+
</Button>
516+
</TooltipTrigger>
517+
<TooltipContent>
518+
<p>{required ? "Required" : "Optional"}</p>
519+
</TooltipContent>
520+
</Tooltip>
533521
{
534522
question?.roles && question?.roles.length > 1 && (
535523
<Tooltip>

0 commit comments

Comments
 (0)