Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor

This commit is contained in:
Piyush Gupta
2024-09-23 23:22:02 +05:30
2 changed files with 142 additions and 85 deletions

View File

@@ -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 && (

View File

@@ -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 &&