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);