mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 13:49:54 -06:00
Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import { CheckIcon, ChevronDownIcon, LucideProps, XIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import {
|
||||
@@ -62,13 +63,23 @@ export const InputCombobox = ({
|
||||
comboboxClasses,
|
||||
emptyDropdownText = "No option found.",
|
||||
}: InputComboboxProps) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [localValue, setLocalValue] = React.useState<
|
||||
TComboboxOption | TComboboxOption[] | string | number | null
|
||||
>(null);
|
||||
const [inputType, setInputType] = React.useState<"dropdown" | "input" | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<TComboboxOption | TComboboxOption[] | string | number | null>(
|
||||
null
|
||||
);
|
||||
const [inputType, setInputType] = useState<"dropdown" | "input" | null>(null);
|
||||
const [inputValue, setInputValue] = useState(value || "");
|
||||
|
||||
showCheckIcon = allowMultiSelect ? true : showCheckIcon;
|
||||
// Debounced function to call onChangeValue
|
||||
const debouncedOnChangeValue = useMemo(
|
||||
() => debounce((val) => onChangeValue(val, undefined, true), 300),
|
||||
[onChangeValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Sync inputValue when value changes externally
|
||||
setInputValue(value || "");
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const validOptions = options?.length ? options : groupedOptions?.flatMap((group) => group.options);
|
||||
@@ -140,8 +151,9 @@ export const InputCombobox = ({
|
||||
const value = e.target.value;
|
||||
|
||||
if (value === "") {
|
||||
setLocalValue(null);
|
||||
onChangeValue("");
|
||||
setLocalValue("");
|
||||
setInputValue("");
|
||||
debouncedOnChangeValue("");
|
||||
}
|
||||
|
||||
if (inputType !== "input") {
|
||||
@@ -150,8 +162,12 @@ export const InputCombobox = ({
|
||||
|
||||
const val = inputType === "number" ? Number(value) : value;
|
||||
|
||||
// Set the local input value immediately
|
||||
setInputValue(val);
|
||||
setLocalValue(val);
|
||||
onChangeValue(val, undefined, true);
|
||||
|
||||
// Trigger the debounced onChangeValue
|
||||
debouncedOnChangeValue(val);
|
||||
};
|
||||
|
||||
const getDisplayValue = useMemo(() => {
|
||||
@@ -196,7 +212,7 @@ export const InputCombobox = ({
|
||||
className="min-w-0 rounded-none border-0 border-r border-slate-300 bg-white focus:border-slate-300"
|
||||
{...inputProps}
|
||||
id={`${id}-input`}
|
||||
value={localValue as string | number}
|
||||
value={inputValue as string | number}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
)}
|
||||
@@ -224,9 +240,7 @@ export const InputCombobox = ({
|
||||
<PopoverContent
|
||||
className={cn(
|
||||
"w-auto max-w-[400px] overflow-y-auto truncate border border-slate-400 bg-slate-50 p-0 shadow-none",
|
||||
{
|
||||
"px-2 pt-2": showSearch,
|
||||
}
|
||||
{ "px-2 pt-2": showSearch }
|
||||
)}>
|
||||
<Command>
|
||||
{showSearch && (
|
||||
@@ -237,7 +251,7 @@ export const InputCombobox = ({
|
||||
)}
|
||||
<CommandList className="mx-1 my-2">
|
||||
<CommandEmpty className="mx-2 my-0">{emptyDropdownText}</CommandEmpty>
|
||||
{options && options.length > 0 ? (
|
||||
{options && options.length > 0 && (
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
@@ -246,13 +260,9 @@ export const InputCombobox = ({
|
||||
title={option.label}
|
||||
className="cursor-pointer truncate hover:text-slate-500">
|
||||
{showCheckIcon &&
|
||||
((allowMultiSelect &&
|
||||
Array.isArray(localValue) &&
|
||||
localValue.find((item) => item.value === option.value)) ||
|
||||
(!allowMultiSelect &&
|
||||
typeof localValue === "object" &&
|
||||
!Array.isArray(localValue) &&
|
||||
localValue?.value === option.value)) && (
|
||||
allowMultiSelect &&
|
||||
Array.isArray(localValue) &&
|
||||
localValue.find((item) => item.value === option.value) && (
|
||||
<CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
|
||||
)}
|
||||
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
|
||||
@@ -269,8 +279,7 @@ export const InputCombobox = ({
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : null}
|
||||
|
||||
)}
|
||||
{groupedOptions?.map((group, idx) => (
|
||||
<>
|
||||
{idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
|
||||
@@ -281,14 +290,10 @@ export const InputCombobox = ({
|
||||
onSelect={() => handleSelect(option)}
|
||||
className="cursor-pointer truncate hover:text-slate-500">
|
||||
{showCheckIcon &&
|
||||
((allowMultiSelect &&
|
||||
Array.isArray(localValue) &&
|
||||
localValue.find((item) => item.value === option.value)) ||
|
||||
(!allowMultiSelect &&
|
||||
typeof localValue === "object" &&
|
||||
!Array.isArray(localValue) &&
|
||||
localValue?.value === option.value)) && (
|
||||
<CheckIcon className="mr-2 h-4 w-4 shrink-0 text-slate-300 hover:text-slate-400" />
|
||||
allowMultiSelect &&
|
||||
Array.isArray(localValue) &&
|
||||
localValue.find((item) => item.value === option.value) && (
|
||||
<CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
|
||||
)}
|
||||
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
|
||||
{option.imgSrc && (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { debounce } from "lodash";
|
||||
import { ImagePlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { extractLanguageCodes, getEnabledLanguages, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
@@ -117,7 +118,7 @@ export const QuestionFormInput = ({
|
||||
[value, id, isInvalid, surveyLanguageCodes]
|
||||
);
|
||||
|
||||
const getElementTextBasedOnType = (): TI18nString => {
|
||||
const getElementTextBasedOnType = useCallback((): TI18nString => {
|
||||
if (isChoice && typeof index === "number") {
|
||||
return getChoiceLabel(question, index, surveyLanguageCodes);
|
||||
}
|
||||
@@ -138,9 +139,22 @@ export const QuestionFormInput = ({
|
||||
(question && (question[id as keyof TSurveyQuestion] as TI18nString)) ||
|
||||
createI18nString("", surveyLanguageCodes)
|
||||
);
|
||||
};
|
||||
}, [
|
||||
id,
|
||||
index,
|
||||
isChoice,
|
||||
isEndingCard,
|
||||
isMatrixLabelColumn,
|
||||
isMatrixLabelRow,
|
||||
isWelcomeCard,
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
surveyLanguageCodes,
|
||||
]);
|
||||
|
||||
const [text, setText] = useState(getElementTextBasedOnType());
|
||||
// const [debouncedText, setDebouncedText] = useState(text); // Added debouncedText state
|
||||
const [renderedText, setRenderedText] = useState<JSX.Element[]>();
|
||||
const [showImageUploader, setShowImageUploader] = useState<boolean>(
|
||||
determineImageUploaderVisibility(questionIdx, localSurvey)
|
||||
@@ -356,50 +370,79 @@ export const QuestionFormInput = ({
|
||||
// choice -> updateChoice
|
||||
// matrixLabel -> updateMatrixLabel
|
||||
|
||||
const handleUpdate = (updatedText: string) => {
|
||||
const translatedText = createUpdatedText(updatedText);
|
||||
const createUpdatedText = useCallback(
|
||||
(updatedText: string): TI18nString => {
|
||||
return {
|
||||
...getElementTextBasedOnType(),
|
||||
[usedLanguageCode]: updatedText,
|
||||
};
|
||||
},
|
||||
[getElementTextBasedOnType, usedLanguageCode]
|
||||
);
|
||||
|
||||
if (isChoice) {
|
||||
updateChoiceDetails(translatedText);
|
||||
} else if (isEndingCard || isWelcomeCard) {
|
||||
updateSurveyDetails(translatedText);
|
||||
} else if (isMatrixLabelRow || isMatrixLabelColumn) {
|
||||
updateMatrixLabelDetails(translatedText);
|
||||
} else {
|
||||
updateQuestionDetails(translatedText);
|
||||
}
|
||||
};
|
||||
const updateChoiceDetails = useCallback(
|
||||
(translatedText: TI18nString) => {
|
||||
if (updateChoice && typeof index === "number") {
|
||||
updateChoice(index, { label: translatedText });
|
||||
}
|
||||
},
|
||||
[index, updateChoice]
|
||||
);
|
||||
|
||||
const createUpdatedText = (updatedText: string): TI18nString => {
|
||||
return {
|
||||
...getElementTextBasedOnType(),
|
||||
[usedLanguageCode]: updatedText,
|
||||
};
|
||||
};
|
||||
const updateSurveyDetails = useCallback(
|
||||
(translatedText: TI18nString) => {
|
||||
if (updateSurvey) {
|
||||
updateSurvey({ [id]: translatedText });
|
||||
}
|
||||
},
|
||||
[id, updateSurvey]
|
||||
);
|
||||
|
||||
const updateChoiceDetails = (translatedText: TI18nString) => {
|
||||
if (updateChoice && typeof index === "number") {
|
||||
updateChoice(index, { label: translatedText });
|
||||
}
|
||||
};
|
||||
const updateMatrixLabelDetails = useCallback(
|
||||
(translatedText: TI18nString) => {
|
||||
if (updateMatrixLabel && typeof index === "number") {
|
||||
updateMatrixLabel(index, isMatrixLabelRow ? "row" : "column", translatedText);
|
||||
}
|
||||
},
|
||||
[index, isMatrixLabelRow, updateMatrixLabel]
|
||||
);
|
||||
|
||||
const updateSurveyDetails = (translatedText: TI18nString) => {
|
||||
if (updateSurvey) {
|
||||
updateSurvey({ [id]: translatedText });
|
||||
}
|
||||
};
|
||||
const updateQuestionDetails = useCallback(
|
||||
(translatedText: TI18nString) => {
|
||||
if (updateQuestion) {
|
||||
updateQuestion(questionIdx, { [id]: translatedText });
|
||||
}
|
||||
},
|
||||
[id, questionIdx, updateQuestion]
|
||||
);
|
||||
|
||||
const updateMatrixLabelDetails = (translatedText: TI18nString) => {
|
||||
if (updateMatrixLabel && typeof index === "number") {
|
||||
updateMatrixLabel(index, isMatrixLabelRow ? "row" : "column", translatedText);
|
||||
}
|
||||
};
|
||||
const handleUpdate = useCallback(
|
||||
(updatedText: string) => {
|
||||
const translatedText = createUpdatedText(updatedText);
|
||||
|
||||
const updateQuestionDetails = (translatedText: TI18nString) => {
|
||||
if (updateQuestion) {
|
||||
updateQuestion(questionIdx, { [id]: translatedText });
|
||||
}
|
||||
};
|
||||
if (isChoice) {
|
||||
updateChoiceDetails(translatedText);
|
||||
} else if (isEndingCard || isWelcomeCard) {
|
||||
updateSurveyDetails(translatedText);
|
||||
} else if (isMatrixLabelRow || isMatrixLabelColumn) {
|
||||
updateMatrixLabelDetails(translatedText);
|
||||
} else {
|
||||
updateQuestionDetails(translatedText);
|
||||
}
|
||||
},
|
||||
[
|
||||
createUpdatedText,
|
||||
isChoice,
|
||||
isEndingCard,
|
||||
isMatrixLabelColumn,
|
||||
isMatrixLabelRow,
|
||||
isWelcomeCard,
|
||||
updateChoiceDetails,
|
||||
updateMatrixLabelDetails,
|
||||
updateQuestionDetails,
|
||||
updateSurveyDetails,
|
||||
]
|
||||
);
|
||||
|
||||
const getFileUrl = (): string | undefined => {
|
||||
if (isWelcomeCard) return localSurvey.welcomeCard.fileUrl;
|
||||
@@ -417,6 +460,20 @@ export const QuestionFormInput = ({
|
||||
} else return question.videoUrl;
|
||||
};
|
||||
|
||||
const debouncedHandleUpdate = useMemo(
|
||||
() => debounce((value) => handleUpdate(headlineToRecall(value, recallItems, fallbacks)), 300),
|
||||
[handleUpdate, recallItems, fallbacks]
|
||||
);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedText = {
|
||||
...getElementTextBasedOnType(),
|
||||
[usedLanguageCode]: e.target.value,
|
||||
};
|
||||
setText(recallToHeadline(updatedText, localSurvey, false, usedLanguageCode, attributeClasses));
|
||||
debouncedHandleUpdate(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full">
|
||||
@@ -454,7 +511,9 @@ export const QuestionFormInput = ({
|
||||
<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 ${localSurvey.languages?.length > 1 ? "pr-24" : ""}`}
|
||||
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 ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
}`}
|
||||
dir="auto">
|
||||
{renderedText}
|
||||
</div>
|
||||
@@ -472,7 +531,9 @@ export const QuestionFormInput = ({
|
||||
<Input
|
||||
key={`${questionId}-${id}-${usedLanguageCode}`}
|
||||
dir="auto"
|
||||
className={`absolute top-0 text-black caret-black ${localSurvey.languages?.length > 1 ? "pr-24" : ""} ${className}`}
|
||||
className={`absolute top-0 text-black caret-black ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
} ${className}`}
|
||||
placeholder={placeholder ? placeholder : getPlaceHolderById(id)}
|
||||
id={id}
|
||||
name={id}
|
||||
@@ -483,18 +544,9 @@ export const QuestionFormInput = ({
|
||||
usedLanguageCode
|
||||
]
|
||||
}
|
||||
onChange={handleInputChange}
|
||||
ref={inputRef}
|
||||
onBlur={onBlur}
|
||||
onChange={(e) => {
|
||||
let translatedText = {
|
||||
...getElementTextBasedOnType(),
|
||||
[usedLanguageCode]: e.target.value,
|
||||
};
|
||||
setText(
|
||||
recallToHeadline(translatedText, localSurvey, false, usedLanguageCode, attributeClasses)
|
||||
);
|
||||
handleUpdate(headlineToRecall(e.target.value, recallItems, fallbacks));
|
||||
}}
|
||||
maxLength={maxLength ?? undefined}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
|
||||
Reference in New Issue
Block a user