diff --git a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx index 98e3be3a1b..1b03f87f64 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx @@ -1,7 +1,7 @@ "use client"; +import { useMemo, useTransition } from "react"; import type { Dispatch, SetStateAction } from "react"; -import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { TI18nString } from "@formbricks/types/i18n"; import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types"; @@ -74,6 +74,8 @@ export function LocalizedEditor({ [id, isInvalid, localSurvey.languages, value] ); + const [, startTransition] = useTransition(); + return (
]*>(.*?)<\/a>/gi, "$1"); } - // Check if the elements still exists before updating const currentElement = elements[elementIdx]; - // if this is a card, we wanna check if the card exists in the localSurvey - if (isCard) { - const isWelcomeCard = elementIdx === -1; - const isEndingCard = elementIdx >= elements.length; + startTransition(() => { + // if this is a card, we wanna check if the card exists in the localSurvey + if (isCard) { + const isWelcomeCard = elementIdx === -1; + const isEndingCard = elementIdx >= elements.length; - // For ending cards, check if the field exists before updating - if (isEndingCard) { - const ending = localSurvey.endings.find((ending) => ending.id === elementId); - // If the field doesn't exist on the ending card, don't create it - if (!ending || ending[id] === undefined) { + // For ending cards, check if the field exists before updating + if (isEndingCard) { + const ending = localSurvey.endings.find((ending) => ending.id === elementId); + // If the field doesn't exist on the ending card, don't create it + if (!ending || ending[id] === undefined) { + return; + } + } + + // For welcome cards, check if it exists + if (isWelcomeCard && !localSurvey.welcomeCard) { return; } - } - // For welcome cards, check if it exists - if (isWelcomeCard && !localSurvey.welcomeCard) { + const translatedContent = { + ...value, + [selectedLanguageCode]: sanitizedContent, + }; + updateElement({ [id]: translatedContent }); return; } - const translatedContent = { - ...value, - [selectedLanguageCode]: sanitizedContent, - }; - updateElement({ [id]: translatedContent }); - return; - } - - // Check if the field exists on the element (not just if it's not undefined) - if (currentElement && id in currentElement && currentElement[id] !== undefined) { - const translatedContent = { - ...value, - [selectedLanguageCode]: sanitizedContent, - }; - updateElement(elementIdx, { [id]: translatedContent }); - } + // Check if the field exists on the element (not just if it's not undefined) + if (currentElement && id in currentElement && currentElement[id] !== undefined) { + const translatedContent = { + ...value, + [selectedLanguageCode]: sanitizedContent, + }; + updateElement(elementIdx, { [id]: translatedContent }); + } + }); }} localSurvey={localSurvey} elementId={elementId} diff --git a/apps/web/modules/survey/editor/components/elements-view.tsx b/apps/web/modules/survey/editor/components/elements-view.tsx index a5a7936a11..add3715ce4 100644 --- a/apps/web/modules/survey/editor/components/elements-view.tsx +++ b/apps/web/modules/survey/editor/components/elements-view.tsx @@ -54,7 +54,7 @@ import { } from "@/modules/survey/editor/lib/utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; -import { isEndingCardValid, isWelcomeCardValid, validateSurveyElementsInBatch } from "../lib/validation"; +import { isEndingCardValid, isWelcomeCardValid, validateElement } from "../lib/validation"; interface ElementsViewProps { localSurvey: TSurvey; @@ -211,35 +211,6 @@ export const ElementsView = ({ }; }; - useEffect(() => { - if (!invalidElements) return; - let updatedInvalidElements: string[] = [...invalidElements]; - - // Check welcome card - if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) { - if (!updatedInvalidElements.includes("start")) { - updatedInvalidElements = [...updatedInvalidElements, "start"]; - } - } else { - updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== "start"); - } - - // Check thank you card - localSurvey.endings.forEach((ending) => { - if (!isEndingCardValid(ending, surveyLanguages)) { - if (!updatedInvalidElements.includes(ending.id)) { - updatedInvalidElements = [...updatedInvalidElements, ending.id]; - } - } else { - updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== ending.id); - } - }); - - if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) { - setInvalidElements(updatedInvalidElements); - } - }, [localSurvey.welcomeCard, localSurvey.endings, surveyLanguages, invalidElements, setInvalidElements]); - const updateElement = (elementIdx: number, updatedAttributes: any) => { // Get element ID from current elements array (for validation) const element = elements[elementIdx]; @@ -250,7 +221,6 @@ export const ElementsView = ({ // Track side effects that need to happen after state update let newActiveElementId: string | null = null; - let invalidElementsUpdate: string[] | null = null; // Use functional update to ensure we work with the latest state setLocalSurvey((prevSurvey) => { @@ -296,13 +266,6 @@ export const ElementsView = ({ const initialElementId = elementId; updatedSurvey = handleElementLogicChange(updatedSurvey, initialElementId, elementLevelAttributes.id); - // Track side effects to apply after state update - if (invalidElements?.includes(initialElementId)) { - invalidElementsUpdate = invalidElements.map((id) => - id === initialElementId ? elementLevelAttributes.id : id - ); - } - // Track new active element ID newActiveElementId = elementLevelAttributes.id; @@ -344,9 +307,6 @@ export const ElementsView = ({ }); // Apply side effects after state update is queued - if (invalidElementsUpdate) { - setInvalidElements(invalidElementsUpdate); - } if (newActiveElementId) { setActiveElementId(newActiveElementId); } @@ -764,23 +724,67 @@ export const ElementsView = ({ setLocalSurvey(result.data); }; - //useEffect to validate survey when changes are made to languages - useEffect(() => { - if (!invalidElements) return; - let updatedInvalidElements: string[] = invalidElements; - // Validate each element - elements.forEach((element) => { - updatedInvalidElements = validateSurveyElementsInBatch( - element, - updatedInvalidElements, - surveyLanguages - ); - }); + // Validate survey when changes are made to languages or elements + // using set for O(1) lookup + useEffect( + () => { + if (!invalidElements) return; - if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) { - setInvalidElements(updatedInvalidElements); - } - }, [elements, surveyLanguages, invalidElements, setInvalidElements]); + const currentInvalidSet = new Set(invalidElements); + let hasChanges = false; + + // Validate each element + elements.forEach((element) => { + const isValid = validateElement(element, surveyLanguages); + if (isValid) { + if (currentInvalidSet.has(element.id)) { + currentInvalidSet.delete(element.id); + hasChanges = true; + } + } else if (!currentInvalidSet.has(element.id)) { + currentInvalidSet.add(element.id); + hasChanges = true; + } + }); + + // Check welcome card + if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) { + if (!currentInvalidSet.has("start")) { + currentInvalidSet.add("start"); + hasChanges = true; + } + } else if (currentInvalidSet.has("start")) { + currentInvalidSet.delete("start"); + hasChanges = true; + } + + // Check thank you card + localSurvey.endings.forEach((ending) => { + if (!isEndingCardValid(ending, surveyLanguages)) { + if (!currentInvalidSet.has(ending.id)) { + currentInvalidSet.add(ending.id); + hasChanges = true; + } + } else if (currentInvalidSet.has(ending.id)) { + currentInvalidSet.delete(ending.id); + hasChanges = true; + } + }); + + if (hasChanges) { + setInvalidElements(Array.from(currentInvalidSet)); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + elements, + surveyLanguages, + invalidElements, + setInvalidElements, + localSurvey.welcomeCard, + localSurvey.endings, + ] + ); useEffect(() => { const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode); @@ -791,7 +795,7 @@ export const ElementsView = ({ } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeElementId, setActiveElementId]); + }, [activeElementId, setActiveElementId, localSurvey, selectedLanguageCode]); const sensors = useSensors( useSensor(PointerSensor, { diff --git a/apps/web/modules/survey/editor/components/survey-editor.tsx b/apps/web/modules/survey/editor/components/survey-editor.tsx index 249b4e74ce..b168a6106e 100644 --- a/apps/web/modules/survey/editor/components/survey-editor.tsx +++ b/apps/web/modules/survey/editor/components/survey-editor.tsx @@ -86,6 +86,7 @@ export const SurveyEditor = ({ const [activeElementId, setActiveElementId] = useState(null); const [localSurvey, setLocalSurvey] = useState(() => structuredClone(survey)); const [invalidElements, setInvalidElements] = useState(null); + const [selectedLanguageCode, setSelectedLanguageCode] = useState("default"); const surveyEditorRef = useRef(null); const [localProject, setLocalProject] = useState(project);