mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-29 03:13:19 -05:00
feat: Recall functionality (#1789)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
e30f16cec2
commit
f2800632e3
@@ -0,0 +1,70 @@
|
||||
import { RefObject } from "react";
|
||||
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { Button } from "../../Button";
|
||||
import { Input } from "../../Input";
|
||||
|
||||
interface FallbackInputProps {
|
||||
filteredRecallQuestions: (TSurveyQuestion | undefined)[];
|
||||
fallbacks: { [type: string]: string };
|
||||
setFallbacks: (fallbacks: { [type: string]: string }) => void;
|
||||
fallbackInputRef: RefObject<HTMLInputElement>;
|
||||
addFallback: () => void;
|
||||
}
|
||||
|
||||
export function FallbackInput({
|
||||
filteredRecallQuestions,
|
||||
fallbacks,
|
||||
setFallbacks,
|
||||
fallbackInputRef,
|
||||
addFallback,
|
||||
}: FallbackInputProps) {
|
||||
return (
|
||||
<div className="fixed z-30 mt-1 rounded-md border border-slate-300 bg-slate-50 p-3 text-xs">
|
||||
<p className="font-medium">Add a placeholder to show if the question gets skipped:</p>
|
||||
{filteredRecallQuestions.map((recallQuestion) => {
|
||||
if (!recallQuestion) return;
|
||||
return (
|
||||
<div className="mt-2 flex flex-col" key={recallQuestion.id}>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="placeholder:text-md h-full bg-white"
|
||||
ref={fallbackInputRef}
|
||||
id="fallback"
|
||||
value={fallbacks[recallQuestion.id]?.replaceAll("nbsp", " ")}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
e.preventDefault();
|
||||
addFallback();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const newFallbacks = { ...fallbacks };
|
||||
newFallbacks[recallQuestion.id] = e.target.value;
|
||||
setFallbacks(newFallbacks);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
className="mt-2 h-full py-2"
|
||||
disabled={
|
||||
Object.values(fallbacks)
|
||||
.map((value) => value.trim())
|
||||
.includes("") || Object.entries(fallbacks).length === 0
|
||||
}
|
||||
variant="darkCTA"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
addFallback();
|
||||
}}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
ListBulletIcon,
|
||||
PhoneIcon,
|
||||
PresentationChartBarIcon,
|
||||
QueueListIcon,
|
||||
StarIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { RefObject, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { replaceRecallInfoWithUnderline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
const questionIconMapping = {
|
||||
openText: ChatBubbleBottomCenterTextIcon,
|
||||
multipleChoiceSingle: QueueListIcon,
|
||||
multipleChoiceMulti: ListBulletIcon,
|
||||
rating: StarIcon,
|
||||
nps: PresentationChartBarIcon,
|
||||
date: CalendarDaysIcon,
|
||||
cal: PhoneIcon,
|
||||
};
|
||||
|
||||
interface RecallQuestionSelectProps {
|
||||
localSurvey: TSurvey;
|
||||
questionId: string;
|
||||
addRecallQuestion: (question: TSurveyQuestion) => void;
|
||||
setShowQuestionSelect: (show: boolean) => void;
|
||||
showQuestionSelect: boolean;
|
||||
inputRef: RefObject<HTMLInputElement>;
|
||||
recallQuestions: TSurveyQuestion[];
|
||||
}
|
||||
|
||||
export default function RecallQuestionSelect({
|
||||
localSurvey,
|
||||
questionId,
|
||||
addRecallQuestion,
|
||||
setShowQuestionSelect,
|
||||
showQuestionSelect,
|
||||
inputRef,
|
||||
recallQuestions,
|
||||
}: RecallQuestionSelectProps) {
|
||||
const [focusedQuestionIdx, setFocusedQuestionIdx] = useState(0); // New state for managing focus
|
||||
const isNotAllowedQuestionType = (question: TSurveyQuestion) => {
|
||||
return (
|
||||
question.type === "fileUpload" ||
|
||||
question.type === "cta" ||
|
||||
question.type === "consent" ||
|
||||
question.type === "pictureSelection" ||
|
||||
question.type === "cal"
|
||||
);
|
||||
};
|
||||
|
||||
const recallQuestionIds = useMemo(() => {
|
||||
return recallQuestions.map((recallQuestion) => recallQuestion.id);
|
||||
}, [recallQuestions]);
|
||||
|
||||
// function to remove some specific type of questions (fileUpload, imageSelect etc) from the list of questions to recall from and few other checks
|
||||
const filteredRecallQuestions = useMemo(() => {
|
||||
const idx =
|
||||
questionId === "end"
|
||||
? localSurvey.questions.length
|
||||
: localSurvey.questions.findIndex((recallQuestion) => recallQuestion.id === questionId);
|
||||
const filteredQuestions = localSurvey.questions.filter((question, index) => {
|
||||
const notAllowed = isNotAllowedQuestionType(question);
|
||||
return (
|
||||
!recallQuestionIds.includes(question.id) && !notAllowed && question.id !== questionId && idx > index
|
||||
);
|
||||
});
|
||||
return filteredQuestions;
|
||||
}, [localSurvey.questions, questionId, recallQuestionIds]);
|
||||
|
||||
// function to modify headline (recallInfo to corresponding headline)
|
||||
const getRecallHeadline = (question: TSurveyQuestion): TSurveyQuestion => {
|
||||
let questionTemp = { ...question };
|
||||
questionTemp = replaceRecallInfoWithUnderline(questionTemp);
|
||||
return questionTemp;
|
||||
};
|
||||
|
||||
// function to handle key press
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (showQuestionSelect) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setFocusedQuestionIdx((prevIdx) => (prevIdx + 1) % filteredRecallQuestions.length);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setFocusedQuestionIdx((prevIdx) =>
|
||||
prevIdx === 0 ? filteredRecallQuestions.length - 1 : prevIdx - 1
|
||||
);
|
||||
} else if (event.key === "Enter") {
|
||||
const selectedQuestion = filteredRecallQuestions[focusedQuestionIdx];
|
||||
setShowQuestionSelect(false);
|
||||
if (!selectedQuestion) return;
|
||||
addRecallQuestion(selectedQuestion);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inputElement = inputRef.current;
|
||||
inputElement?.addEventListener("keydown", handleKeyPress);
|
||||
|
||||
return () => {
|
||||
inputElement?.removeEventListener("keydown", handleKeyPress);
|
||||
};
|
||||
}, [showQuestionSelect, localSurvey.questions, focusedQuestionIdx]);
|
||||
|
||||
return (
|
||||
<div className="absolute z-30 mt-1 flex max-h-[50%] max-w-[85%] flex-col overflow-y-auto rounded-md border border-slate-300 bg-slate-50 p-3 text-xs ">
|
||||
{filteredRecallQuestions.length === 0 ? (
|
||||
<p className="font-medium text-slate-900">There is no information to recall yet 🤷</p>
|
||||
) : (
|
||||
<p className="mb-2 font-medium">Recall Information from...</p>
|
||||
)}
|
||||
<div>
|
||||
{filteredRecallQuestions.map((q, idx) => {
|
||||
const isFocused = idx === focusedQuestionIdx;
|
||||
const IconComponent = questionIconMapping[q.type as keyof typeof questionIconMapping];
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
className={`flex max-w-full cursor-pointer items-center rounded-md px-3 py-2 ${
|
||||
isFocused ? "bg-slate-200" : "hover:bg-slate-200 "
|
||||
}`}
|
||||
onClick={() => {
|
||||
addRecallQuestion(q);
|
||||
setShowQuestionSelect(false);
|
||||
}}>
|
||||
<div>{IconComponent && <IconComponent className="mr-2 w-4" />}</div>
|
||||
<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{getRecallHeadline(q).headline}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
"use client";
|
||||
|
||||
import { PencilIcon } from "@heroicons/react/24/solid";
|
||||
import { ImagePlusIcon } from "lucide-react";
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
extractId,
|
||||
extractRecallInfo,
|
||||
findRecallInfoById,
|
||||
getFallbackValues,
|
||||
getRecallQuestions,
|
||||
headlineToRecall,
|
||||
recallToHeadline,
|
||||
replaceRecallInfoWithUnderline,
|
||||
useSyncScroll,
|
||||
} from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import FileInput from "../FileInput";
|
||||
import { Input } from "../Input";
|
||||
import { Label } from "../Label";
|
||||
import { FallbackInput } from "./components/FallbackInput";
|
||||
import RecallQuestionSelect from "./components/RecallQuestionSelect";
|
||||
|
||||
interface QuestionFormInputProps {
|
||||
localSurvey: TSurvey;
|
||||
questionId: string;
|
||||
questionIdx: number;
|
||||
updateQuestion?: (questionIdx: number, data: Partial<TSurveyQuestion>) => void;
|
||||
updateSurvey?: (data: Partial<TSurveyQuestion>) => void;
|
||||
environmentId: string;
|
||||
type: string;
|
||||
isInvalid?: boolean;
|
||||
ref?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const QuestionFormInput = ({
|
||||
localSurvey,
|
||||
questionId,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
updateSurvey,
|
||||
isInvalid,
|
||||
environmentId,
|
||||
type,
|
||||
}: QuestionFormInputProps) => {
|
||||
const isThankYouCard = questionId === "end";
|
||||
const question = useMemo(() => {
|
||||
return isThankYouCard
|
||||
? localSurvey.thankYouCard
|
||||
: localSurvey.questions.find((question) => question.id === questionId)!;
|
||||
}, [isThankYouCard, localSurvey, questionId]);
|
||||
|
||||
const getQuestionTextBasedOnType = (): string => {
|
||||
return question[type as keyof typeof question] || "";
|
||||
};
|
||||
|
||||
const [text, setText] = useState(getQuestionTextBasedOnType() ?? "");
|
||||
const [renderedText, setRenderedText] = useState<JSX.Element[]>();
|
||||
|
||||
const highlightContainerRef = useRef<HTMLInputElement>(null);
|
||||
const fallbackInputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [showImageUploader, setShowImageUploader] = useState<boolean>(
|
||||
questionId === "end" ? false : !!(question as TSurveyQuestion).imageUrl
|
||||
);
|
||||
const [showQuestionSelect, setShowQuestionSelect] = useState(false);
|
||||
const [showFallbackInput, setShowFallbackInput] = useState(false);
|
||||
const [recallQuestions, setRecallQuestions] = useState<TSurveyQuestion[]>(
|
||||
text.includes("#recall:") ? getRecallQuestions(text, localSurvey) : []
|
||||
);
|
||||
const filteredRecallQuestions = Array.from(new Set(recallQuestions.map((q) => q.id))).map((id) => {
|
||||
return recallQuestions.find((q) => q.id === id);
|
||||
});
|
||||
const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>(
|
||||
text.includes("/fallback:") ? getFallbackValues(text) : {}
|
||||
);
|
||||
|
||||
// Hook to synchronize the horizontal scroll position of highlightContainerRef and inputRef.
|
||||
useSyncScroll(highlightContainerRef, inputRef, text);
|
||||
|
||||
useEffect(() => {
|
||||
// Generates an array of headlines from recallQuestions, replacing nested recall questions with '___' .
|
||||
const recallQuestionHeadlines = recallQuestions.flatMap((recallQuestion) => {
|
||||
if (!recallQuestion.headline.includes("#recall:")) {
|
||||
return [recallQuestion.headline];
|
||||
}
|
||||
const recallQuestionText = (recallQuestion[type as keyof typeof recallQuestion] as string) || "";
|
||||
const recallInfo = extractRecallInfo(recallQuestionText);
|
||||
|
||||
if (recallInfo) {
|
||||
const recallQuestionId = extractId(recallInfo);
|
||||
const recallQuestion = localSurvey.questions.find((question) => question.id === recallQuestionId);
|
||||
|
||||
if (recallQuestion) {
|
||||
return [recallQuestionText.replace(recallInfo, `___`)];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// Constructs an array of JSX elements representing segmented parts of text, interspersed with special formatted spans for recall headlines.
|
||||
const processInput = (): JSX.Element[] => {
|
||||
const parts: JSX.Element[] = [];
|
||||
let remainingText: string = text ?? "";
|
||||
remainingText = recallToHeadline(remainingText, localSurvey, false);
|
||||
filterRecallQuestions(remainingText);
|
||||
recallQuestionHeadlines.forEach((headline) => {
|
||||
const index = remainingText.indexOf("@" + headline);
|
||||
if (index !== -1) {
|
||||
if (index > 0) {
|
||||
parts.push(
|
||||
<span key={parts.length} className="whitespace-pre">
|
||||
{remainingText.substring(0, index)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
parts.push(
|
||||
<span
|
||||
className="z-30 flex cursor-pointer items-center justify-center whitespace-pre rounded-md bg-slate-100 text-sm text-transparent"
|
||||
key={parts.length}>
|
||||
{"@" + headline}
|
||||
</span>
|
||||
);
|
||||
remainingText = remainingText.substring(index + headline.length + 1);
|
||||
}
|
||||
});
|
||||
if (remainingText.length) {
|
||||
parts.push(
|
||||
<span className="whitespace-pre" key={parts.length}>
|
||||
{remainingText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
setRenderedText(processInput());
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fallbackInputRef.current) {
|
||||
fallbackInputRef.current.focus();
|
||||
}
|
||||
}, [showFallbackInput]);
|
||||
|
||||
const checkForRecallSymbol = () => {
|
||||
const pattern = /(^|\s)@(\s|$)/;
|
||||
if (pattern.test(text)) {
|
||||
setShowQuestionSelect(true);
|
||||
} else {
|
||||
setShowQuestionSelect(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Adds a new recall question to the recallQuestions array, updates fallbacks, modifies the text with recall details.
|
||||
const addRecallQuestion = (recallQuestion: TSurveyQuestion) => {
|
||||
let recallQuestionTemp = { ...recallQuestion };
|
||||
recallQuestionTemp = replaceRecallInfoWithUnderline(recallQuestionTemp);
|
||||
setRecallQuestions((prevQuestions) => {
|
||||
const updatedQuestions = [...prevQuestions, recallQuestionTemp];
|
||||
return updatedQuestions;
|
||||
});
|
||||
if (!Object.keys(fallbacks).includes(recallQuestion.id)) {
|
||||
setFallbacks((prevFallbacks) => ({
|
||||
...prevFallbacks,
|
||||
[recallQuestion.id]: "",
|
||||
}));
|
||||
}
|
||||
setShowQuestionSelect(false);
|
||||
const modifiedHeadlineWithId = getQuestionTextBasedOnType().replace(
|
||||
"@",
|
||||
`#recall:${recallQuestion.id}/fallback:# `
|
||||
);
|
||||
updateQuestionDetails(modifiedHeadlineWithId);
|
||||
|
||||
const modifiedHeadlineWithName = recallToHeadline(modifiedHeadlineWithId, localSurvey, false);
|
||||
setText(modifiedHeadlineWithName);
|
||||
setShowFallbackInput(true);
|
||||
};
|
||||
|
||||
// Filters and updates the list of recall questions based on their presence in the given text, also managing related text and fallback states.
|
||||
const filterRecallQuestions = (text: string) => {
|
||||
let includedQuestions: TSurveyQuestion[] = [];
|
||||
recallQuestions.forEach((recallQuestion) => {
|
||||
if (text.includes(`@${recallQuestion.headline}`)) {
|
||||
includedQuestions.push(recallQuestion);
|
||||
} else {
|
||||
const questionToRemove = recallQuestion.headline.slice(0, -1);
|
||||
const newText = text.replace(`@${questionToRemove}`, "");
|
||||
setText(newText);
|
||||
updateQuestionDetails(newText);
|
||||
let updatedFallback = { ...fallbacks };
|
||||
delete updatedFallback[recallQuestion.id];
|
||||
setFallbacks(updatedFallback);
|
||||
}
|
||||
});
|
||||
setRecallQuestions(includedQuestions);
|
||||
};
|
||||
|
||||
const addFallback = () => {
|
||||
let headlineWithFallback = getQuestionTextBasedOnType();
|
||||
filteredRecallQuestions.forEach((recallQuestion) => {
|
||||
if (recallQuestion) {
|
||||
const recallInfo = findRecallInfoById(getQuestionTextBasedOnType(), recallQuestion!.id);
|
||||
if (recallInfo) {
|
||||
let fallBackValue = fallbacks[recallQuestion.id].trim();
|
||||
fallBackValue = fallBackValue.replace(/ /g, "nbsp");
|
||||
let updatedFallback = { ...fallbacks };
|
||||
updatedFallback[recallQuestion.id] = fallBackValue;
|
||||
setFallbacks(updatedFallback);
|
||||
headlineWithFallback = headlineWithFallback.replace(
|
||||
recallInfo,
|
||||
`#recall:${recallQuestion?.id}/fallback:${fallBackValue}#`
|
||||
);
|
||||
updateQuestionDetails(headlineWithFallback);
|
||||
}
|
||||
}
|
||||
});
|
||||
setShowFallbackInput(false);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkForRecallSymbol();
|
||||
}, [text]);
|
||||
|
||||
// updation of questions and Thank You Card is done in a different manner, so for question we use updateQuestion and for ThankYouCard we use updateSurvey
|
||||
const updateQuestionDetails = (updatedText: string) => {
|
||||
if (isThankYouCard) {
|
||||
if (updateSurvey) {
|
||||
updateSurvey({ [type]: updatedText });
|
||||
}
|
||||
} else {
|
||||
if (updateQuestion) {
|
||||
updateQuestion(questionIdx, {
|
||||
[type]: updatedText,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-3 w-full">
|
||||
<Label htmlFor="headline">{type === "headline" ? "Question" : "Description"}</Label>
|
||||
<div className="mt-2 flex flex-col gap-6 overflow-hidden">
|
||||
{showImageUploader && (
|
||||
<FileInput
|
||||
id="question-image"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[] | undefined) => {
|
||||
if (updateQuestion && url) {
|
||||
updateQuestion(questionIdx, { imageUrl: url[0] });
|
||||
}
|
||||
}}
|
||||
fileUrl={isThankYouCard ? "" : (question as TSurveyQuestion).imageUrl}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="group relative w-full">
|
||||
<div className="h-10 w-full"></div>
|
||||
<div
|
||||
id="wrapper"
|
||||
ref={highlightContainerRef}
|
||||
className="no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ">
|
||||
{renderedText}
|
||||
</div>
|
||||
{getQuestionTextBasedOnType().includes("recall:") && (
|
||||
<button
|
||||
className="fixed right-14 hidden items-center rounded-b-lg bg-slate-100 px-2.5 py-1 text-xs hover:bg-slate-200 group-hover:flex"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowFallbackInput(true);
|
||||
}}>
|
||||
Edit Recall
|
||||
<PencilIcon className="ml-2 h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
<Input
|
||||
className="absolute top-0 text-black caret-black"
|
||||
placeholder={
|
||||
type === "headline"
|
||||
? "Your question here. Recall information with @"
|
||||
: "Your description here. Recall information with @"
|
||||
}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
id={type}
|
||||
name={type}
|
||||
aria-label={type === "headline" ? "Question" : "Description"}
|
||||
autoComplete={showQuestionSelect ? "off" : "on"}
|
||||
value={recallToHeadline(text ?? "", localSurvey, false)}
|
||||
onChange={(e) => {
|
||||
setText(recallToHeadline(e.target.value ?? "", localSurvey, false));
|
||||
updateQuestionDetails(headlineToRecall(e.target.value, recallQuestions, fallbacks));
|
||||
}}
|
||||
isInvalid={isInvalid && text.trim() === ""}
|
||||
/>
|
||||
{!showQuestionSelect && showFallbackInput && recallQuestions.length > 0 && (
|
||||
<FallbackInput
|
||||
filteredRecallQuestions={filteredRecallQuestions}
|
||||
fallbacks={fallbacks}
|
||||
setFallbacks={setFallbacks}
|
||||
fallbackInputRef={fallbackInputRef}
|
||||
addFallback={addFallback}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{type === "headline" && (
|
||||
<ImagePlusIcon
|
||||
aria-label="Toggle image uploader"
|
||||
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => setShowImageUploader((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showQuestionSelect && (
|
||||
<RecallQuestionSelect
|
||||
localSurvey={localSurvey}
|
||||
questionId={questionId}
|
||||
addRecallQuestion={addRecallQuestion}
|
||||
setShowQuestionSelect={setShowQuestionSelect}
|
||||
showQuestionSelect={showQuestionSelect}
|
||||
inputRef={inputRef}
|
||||
recallQuestions={recallQuestions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default QuestionFormInput;
|
||||
Reference in New Issue
Block a user