11"use client" ;
22
3- import { cn , dateToString } from "@/lib/utils" ;
3+ import { cn } from "@/lib/utils" ;
44import { 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" ;
77import Link from "next/link" ;
88import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs"
99import { createQuestion , deleteQuestion , getAllCommonQuestions , getAllRoleQuestions , MultiOptionQuestionOption , Question , QuestionType , updateQuestion } from "@/models/question" ;
10- import { useEffect , useState } from "react" ;
10+ import { useState } from "react" ;
1111import { Input } from "@/components/ui/input" ;
12- import { Label } from "@/components/ui/label" ;
1312import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select" ;
1413import { DragDropContext , Draggable , Droppable , DropResult } from '@hello-pangea/dnd' ;
1514import { 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
511482function 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