diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx index 6b934fa450..490060e162 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx @@ -13,7 +13,7 @@ import { SplitIcon, TrashIcon, } from "lucide-react"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils"; import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; @@ -42,6 +42,14 @@ export function ConditionalLogic({ questionIdx, updateQuestion, }: ConditionalLogicProps) { + const [questionLogic, setQuestionLogic] = useState(question.logic); + + useEffect(() => { + updateQuestion(questionIdx, { + logic: questionLogic, + }); + }, [questionLogic]); + const transformedSurvey = useMemo(() => { let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses); modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses); @@ -49,6 +57,10 @@ export function ConditionalLogic({ return modifiedSurvey; }, [localSurvey, attributeClasses]); + const updateQuestionLogic = (_questionIdx: number, updatedAttributes: any) => { + setQuestionLogic(updatedAttributes.logic); + }; + const addLogic = () => { const operator = getDefaultOperatorForQuestion(question); @@ -77,37 +89,37 @@ export function ConditionalLogic({ ], }; - updateQuestion(questionIdx, { + updateQuestionLogic(questionIdx, { logic: [...(question?.logic ?? []), initialCondition], }); }; const handleRemoveLogic = (logicItemIdx: number) => { - const logicCopy = structuredClone(question.logic ?? []); + const logicCopy = structuredClone(questionLogic ?? []); logicCopy.splice(logicItemIdx, 1); - updateQuestion(questionIdx, { + updateQuestionLogic(questionIdx, { logic: logicCopy, }); }; const moveLogic = (from: number, to: number) => { - const logicCopy = structuredClone(question.logic ?? []); + const logicCopy = structuredClone(questionLogic ?? []); const [movedItem] = logicCopy.splice(from, 1); logicCopy.splice(to, 0, movedItem); - updateQuestion(questionIdx, { + updateQuestionLogic(questionIdx, { logic: logicCopy, }); }; const duplicateLogic = (logicItemIdx: number) => { - const logicCopy = structuredClone(question.logic ?? []); + const logicCopy = structuredClone(questionLogic ?? []); const logicItem = logicCopy[logicItemIdx]; const newLogicItem = duplicateLogicItem(logicItem); logicCopy.splice(logicItemIdx + 1, 0, newLogicItem); - updateQuestion(questionIdx, { + updateQuestionLogic(questionIdx, { logic: logicCopy, }); }; @@ -119,20 +131,20 @@ export function ConditionalLogic({ - {question.logic && question.logic.length > 0 && ( + {questionLogic && questionLogic.length > 0 && (
- {question.logic.map((logicItem, logicItemIdx) => ( + {questionLogic.map((logicItem, logicItemIdx) => (
@@ -159,7 +171,7 @@ export function ConditionalLogic({ { moveLogic(logicItemIdx, logicItemIdx + 1); }}> diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorAction.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorAction.tsx new file mode 100644 index 0000000000..d5c273a69d --- /dev/null +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorAction.tsx @@ -0,0 +1,241 @@ +import { actionObjectiveOptions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils"; +import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react"; +import React, { useEffect, useMemo } from "react"; +import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; +import { questionIconMapping } from "@formbricks/lib/utils/questions"; +import { + TActionNumberVariableCalculateOperator, + TActionObjective, + TActionTextVariableCalculateOperator, + TActionVariableValueType, + TSurvey, + TSurveyLogicAction, + TSurveyQuestion, +} from "@formbricks/types/surveys/types"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@formbricks/ui/DropdownMenu"; +import { InputCombobox, TComboboxOption } from "@formbricks/ui/InputCombobox"; + +interface LogicEditorActionProps { + action: TSurveyLogicAction; + actionIdx: number; + handleObjectiveChange: (actionIdx: number, val: TActionObjective) => void; + handleValuesChange: (actionIdx: number, values: any) => void; + handleActionsChange: (operation: "remove" | "addBelow" | "duplicate", actionIdx: number) => void; + isRemoveDisabled: boolean; + filteredQuestions: TSurveyQuestion[]; + endings: TSurvey["endings"]; +} + +const _LogicEditorAction = ({ + action, + actionIdx, + handleActionsChange, + handleObjectiveChange, + handleValuesChange, + isRemoveDisabled, + filteredQuestions, + endings, +}: LogicEditorActionProps) => { + useEffect(() => { + console.log("action changed"); + }, [action]); + useEffect(() => { + console.log("filteredQuestions changed"); + }, [filteredQuestions]); + useEffect(() => { + console.log("endings changed"); + }, [endings]); + useEffect(() => { + console.log("isRemoveDisabled changed"); + }, [isRemoveDisabled]); + useEffect(() => { + console.log("actionIdx changed"); + }, [actionIdx]); + useEffect(() => { + console.log("handleActionsChange changed"); + }, [handleActionsChange]); + useEffect(() => { + console.log("handleObjectiveChange changed"); + }, [handleObjectiveChange]); + useEffect(() => { + console.log("handleValuesChange changed"); + }, [handleValuesChange]); + + const actionTargetOptions = useMemo((): TComboboxOption[] => { + // let questions = localSurvey.questions.filter((_, idx) => idx !== questionIdx); + let questions = [...filteredQuestions]; + + if (action.objective === "requireAnswer") { + questions = questions.filter((question) => !question.required); + } + + const questionOptions = questions.map((question) => { + return { + icon: questionIconMapping[question.type], + label: getLocalizedValue(question.headline, "default"), + value: question.id, + }; + }); + + if (action.objective === "requireAnswer") return questionOptions; + + const endingCardOptions = endings.map((ending) => { + return { + label: + ending.type === "endScreen" + ? getLocalizedValue(ending.headline, "default") || "End Screen" + : ending.label || "Redirect Thank you card", + value: ending.id, + }; + }); + + return [...questionOptions, ...endingCardOptions]; + }, [action.objective, JSON.stringify(filteredQuestions), endings]); + + return ( +
+
{actionIdx === 0 ? "Then" : "and"}
+
+ { + handleObjectiveChange(actionIdx, val); + }} + comboboxClasses="grow" + /> + {action.objective !== "calculate" && ( + { + handleValuesChange(actionIdx, { + target: val, + }); + }} + comboboxClasses="grow" + /> + )} + {/* {action.objective === "calculate" && ( + <> + { + handleValuesChange(actionIdx, { + variableId: val, + value: { + type: "static", + value: "", + }, + }); + }} + comboboxClasses="grow" + emptyDropdownText="Add a variable to calculate" + /> + v.id === action.variableId)?.type + )} + value={action.operator} + onChangeValue={( + val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator + ) => { + handleValuesChange(actionIdx, { + operator: val, + }); + }} + comboboxClasses="grow" + /> + v.id === action.variableId)?.type || "text", + }} + groupedOptions={getActionValueOptions(action.variableId, localSurvey)} + onChangeValue={(val, option, fromInput) => { + const fieldType = option?.meta?.type as TActionVariableValueType; + + if (!fromInput && fieldType !== "static") { + handleValuesChange(actionIdx, { + value: { + type: fieldType, + value: val as string, + }, + }); + } else if (fromInput) { + handleValuesChange(actionIdx, { + value: { + type: "static", + value: val as string, + }, + }); + } + }} + comboboxClasses="grow shrink-0" + /> + + )} */} +
+ + + + + + + { + handleActionsChange("addBelow", actionIdx); + }}> + + Add action below + + + { + handleActionsChange("remove", actionIdx); + }}> + + Remove + + + { + handleActionsChange("duplicate", actionIdx); + }}> + + Duplicate + + + +
+ ); +}; + +export const LogicEditorAction = React.memo(_LogicEditorAction); diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx index e33dc22b81..eaa2b6b5fd 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx @@ -1,13 +1,16 @@ +import { LogicEditorAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorAction"; import { actionObjectiveOptions, - getActionOperatorOptions, - getActionTargetOptions, + getActionOperatorOptions, // getActionTargetOptions, getActionValueOptions, getActionVariableOptions, } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils"; import { createId } from "@paralleldrive/cuid2"; import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils"; +import { questionIconMapping } from "@formbricks/lib/utils/questions"; import { TActionNumberVariableCalculateOperator, TActionObjective, @@ -24,7 +27,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@formbricks/ui/DropdownMenu"; -import { InputCombobox } from "@formbricks/ui/InputCombobox"; +import { InputCombobox, TComboboxOption } from "@formbricks/ui/InputCombobox"; interface LogicEditorActions { localSurvey: TSurvey; @@ -35,202 +38,129 @@ interface LogicEditorActions { questionIdx: number; } -export function LogicEditorActions({ +export const LogicEditorActions = ({ localSurvey, logicItem, logicIdx, question, updateQuestion, questionIdx, -}: LogicEditorActions) { +}: LogicEditorActions) => { const actions = logicItem.actions; - const handleActionsChange = ( - operation: "remove" | "addBelow" | "duplicate" | "update", - actionIdx: number, - action?: TSurveyLogicAction - ) => { - const logicCopy = structuredClone(question.logic) ?? []; - const currentLogicItem = logicCopy[logicIdx]; - const actionsClone = currentLogicItem.actions; + const handleActionsChange = useCallback( + ( + operation: "remove" | "addBelow" | "duplicate" | "update", + actionIdx: number, + action?: TSurveyLogicAction + ) => { + const logicCopy = structuredClone(question.logic) ?? []; + const currentLogicItem = logicCopy[logicIdx]; + const actionsClone = currentLogicItem.actions; - switch (operation) { - case "remove": - actionsClone.splice(actionIdx, 1); - break; - case "addBelow": - actionsClone.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" }); - break; - case "duplicate": - actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() }); - break; - case "update": - if (!action) return; - actionsClone[actionIdx] = action; - break; - } + switch (operation) { + case "remove": + actionsClone.splice(actionIdx, 1); + break; + case "addBelow": + actionsClone.splice(actionIdx + 1, 0, { + id: createId(), + objective: "jumpToQuestion", + target: "", + }); + break; + case "duplicate": + actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() }); + break; + case "update": + if (!action) return; + actionsClone[actionIdx] = action; + break; + } - updateQuestion(questionIdx, { - logic: logicCopy, - }); - }; + updateQuestion(questionIdx, { + logic: logicCopy, + }); + }, + [logicIdx, question.logic, questionIdx] + ); - const handleObjectiveChange = (actionIdx: number, objective: TActionObjective) => { - const action = actions[actionIdx]; - const actionBody = getUpdatedActionBody(action, objective); - handleActionsChange("update", actionIdx, actionBody); - }; + const handleObjectiveChange = useCallback( + (actionIdx: number, objective: TActionObjective) => { + const action = actions[actionIdx]; + const actionBody = getUpdatedActionBody(action, objective); + handleActionsChange("update", actionIdx, actionBody); + }, + [actions] + ); - const handleValuesChange = (actionIdx: number, values: Partial) => { - const action = actions[actionIdx]; - const actionBody = { ...action, ...values } as TSurveyLogicAction; - handleActionsChange("update", actionIdx, actionBody); - }; + const handleValuesChange = useCallback( + (actionIdx: number, values: Partial) => { + const action = actions[actionIdx]; + const actionBody = { ...action, ...values } as TSurveyLogicAction; + handleActionsChange("update", actionIdx, actionBody); + }, + [actions] + ); + + const filteredQuestions = useMemo( + () => localSurvey.questions.filter((_, idx) => idx !== questionIdx), + [localSurvey.questions, questionIdx] + ); + + const endings = useMemo(() => localSurvey.endings, [JSON.stringify(localSurvey.endings)]); return (
{actions?.map((action, idx) => ( -
-
{idx === 0 ? "Then" : "and"}
-
- { - handleObjectiveChange(idx, val); - }} - comboboxClasses="grow" - /> - {action.objective !== "calculate" && ( - { - handleValuesChange(idx, { - target: val, - }); - }} - comboboxClasses="grow" - /> - )} - {action.objective === "calculate" && ( - <> - { - handleValuesChange(idx, { - variableId: val, - value: { - type: "static", - value: "", - }, - }); - }} - comboboxClasses="grow" - emptyDropdownText="Add a variable to calculate" - /> - v.id === action.variableId)?.type - )} - value={action.operator} - onChangeValue={( - val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator - ) => { - handleValuesChange(idx, { - operator: val, - }); - }} - comboboxClasses="grow" - /> - v.id === action.variableId)?.type || "text", - }} - groupedOptions={getActionValueOptions(action.variableId, localSurvey)} - onChangeValue={(val, option, fromInput) => { - const fieldType = option?.meta?.type as TActionVariableValueType; - - if (!fromInput && fieldType !== "static") { - handleValuesChange(idx, { - value: { - type: fieldType, - value: val as string, - }, - }); - } else if (fromInput) { - handleValuesChange(idx, { - value: { - type: "static", - value: val as string, - }, - }); - } - }} - comboboxClasses="grow shrink-0" - /> - - )} -
- - - - - - - { - handleActionsChange("addBelow", idx); - }}> - - Add action below - - - { - handleActionsChange("remove", idx); - }}> - - Remove - - - { - handleActionsChange("duplicate", idx); - }}> - - Duplicate - - - -
+ ))}
); -} +}; + +// a code snippet living in a component +// source: https://stackoverflow.com/a/59843241/3600510 +const usePrevious = (value, initialValue) => { + const ref = useRef(initialValue); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; +const useEffectDebugger = (effectHook, dependencies, dependencyNames = []) => { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce((accum, dependency, index) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency, + }, + }; + } + + return accum; + }, {}); + + if (Object.keys(changedDeps).length) { + console.log("[use-effect-debugger] ", changedDeps); + } + + useEffect(effectHook, dependencies); +}; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx index ef7a0d9c37..1b5f36ad65 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx @@ -258,7 +258,7 @@ export function LogicEditorConditions({
)}
- - )} + )} */} diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx index 5e3bdb3d7f..a939af2475 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx @@ -264,6 +264,7 @@ export const QuestionsView = ({ } } }); + setLocalSurvey(updatedSurvey); validateSurveyQuestion(updatedSurvey.questions[questionIdx]); }; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx index a91f3cda24..63757f475d 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx @@ -1,8 +1,8 @@ import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react"; -import { HTMLInputTypeAttribute } from "react"; +import { HTMLInputTypeAttribute, useMemo } from "react"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils"; -import { questionTypes } from "@formbricks/lib/utils/questions"; +import { questionIconMapping, questionTypes } from "@formbricks/lib/utils/questions"; import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { @@ -40,14 +40,6 @@ export const formatTextWithSlashes = (text: string) => { }); }; -const questionIconMapping = questionTypes.reduce( - (prev, curr) => ({ - ...prev, - [curr.id]: curr.icon, - }), - {} -); - export const getConditionValueOptions = ( localSurvey: TSurvey, currQuestionIdx: number @@ -756,40 +748,6 @@ export const getMatchValueProps = ( return { show: false, options: [] }; }; -export const getActionTargetOptions = ( - action: TSurveyLogicAction, - localSurvey: TSurvey, - currQuestionIdx: number -): TComboboxOption[] => { - let questions = localSurvey.questions.filter((_, idx) => idx !== currQuestionIdx); - - if (action.objective === "requireAnswer") { - questions = questions.filter((question) => !question.required); - } - - const questionOptions = questions.map((question) => { - return { - icon: questionIconMapping[question.type], - label: getLocalizedValue(question.headline, "default"), - value: question.id, - }; - }); - - if (action.objective === "requireAnswer") return questionOptions; - - const endingCardOptions = localSurvey.endings.map((ending) => { - return { - label: - ending.type === "endScreen" - ? getLocalizedValue(ending.headline, "default") || "End Screen" - : ending.label || "Redirect Thank you card", - value: ending.id, - }; - }); - - return [...questionOptions, ...endingCardOptions]; -}; - export const getActionVariableOptions = (localSurvey: TSurvey): TComboboxOption[] => { const variables = localSurvey.variables ?? []; diff --git a/packages/lib/utils/questions.tsx b/packages/lib/utils/questions.tsx index 9d1c913d52..5809c7a54a 100644 --- a/packages/lib/utils/questions.tsx +++ b/packages/lib/utils/questions.tsx @@ -231,6 +231,14 @@ export const questionTypes: TQuestion[] = [ }, ]; +export const questionIconMapping = questionTypes.reduce( + (prev, curr) => ({ + ...prev, + [curr.id]: curr.icon, + }), + {} +); + export const CXQuestionTypes = questionTypes.filter((questionType) => { return [ TSurveyQuestionTypeEnum.OpenText, diff --git a/packages/lib/utils/recall.ts b/packages/lib/utils/recall.ts index 65423e68d6..03cb3a2e4e 100644 --- a/packages/lib/utils/recall.ts +++ b/packages/lib/utils/recall.ts @@ -12,7 +12,7 @@ import { getLocalizedValue } from "../i18n/utils"; import { structuredClone } from "../pollyfills/structuredClone"; import { formatDateWithOrdinal, isValidDateString } from "./datetime"; -export interface fallbacks { +export interface TFallbackString { [id: string]: string; } @@ -209,17 +209,19 @@ export const getRecallItems = ( }; // Constructs a fallbacks object from a text containing multiple recall and fallback patterns. -export const getFallbackValues = (text: string): fallbacks => { +export const getFallbackValues = (text: string): TFallbackString => { if (!text.includes("#recall:")) return {}; - const pattern = /#recall:([A-Za-z0-9_-]+)\/fallback:([\S*]+)#/g; - let match; - const fallbacks: fallbacks = {}; - while ((match = pattern.exec(text)) !== null) { + const pattern = /#recall:([A-Za-z0-9_-]+)\/fallback:([\S*]+)#/g; + const fallbacks: TFallbackString = {}; + + let match = pattern.exec(text); + while (match !== null) { const id = match[1]; const fallbackValue = match[2]; fallbacks[id] = fallbackValue; } + return fallbacks; }; @@ -227,7 +229,7 @@ export const getFallbackValues = (text: string): fallbacks => { export const headlineToRecall = ( text: string, recallItems: TSurveyRecallItem[], - fallbacks: fallbacks + fallbacks: TFallbackString ): string => { recallItems.forEach((recallItem) => { const recallInfo = `#recall:${recallItem.id}/fallback:${fallbacks[recallItem.id]}#`; diff --git a/packages/ui/InputCombobox/index.tsx b/packages/ui/InputCombobox/index.tsx index 273791223e..10d19becb4 100644 --- a/packages/ui/InputCombobox/index.tsx +++ b/packages/ui/InputCombobox/index.tsx @@ -82,9 +82,28 @@ export const InputCombobox = ({ setInputValue(value || ""); }, [value]); + // useEffect(() => { + // console.log("changing the options"); + // }, [options]); + // useEffect(() => { + // console.log("changing the groupedOptions"); + // }, [groupedOptions]); + // useEffect(() => { + // console.log("changing the value"); + // }, [value]); + // useEffect(() => { + // console.log("changing the inputType"); + // }, [inputType]); + // useEffect(() => { + // console.log("changing the withInput"); + // }, [withInput]); + useEffect(() => { + // console.log("running this useEffect"); const validOptions = options?.length ? options : groupedOptions?.flatMap((group) => group.options); + console.log({ options, groupedOptions, value, validOptions }); + if (value === null || value === undefined) { setLocalValue(""); setInputType(null); @@ -158,11 +177,11 @@ export const InputCombobox = ({ if (value === "") { setLocalValue(""); setInputValue(""); - if (!isE2E) { - debouncedOnChangeValue(""); - } else { - onChangeValue("", undefined, true); - } + // if (!isE2E) { + // debouncedOnChangeValue(""); + // } else { + onChangeValue("", undefined, true); + // } } if (inputType !== "input") { @@ -177,11 +196,11 @@ export const InputCombobox = ({ // Trigger the debounced onChangeValue - if (!isE2E) { - debouncedOnChangeValue(val); - } else { - onChangeValue(val, undefined, true); - } + // if (!isE2E) { + // debouncedOnChangeValue(val); + // } else { + onChangeValue(val, undefined, true); + // } }; const getDisplayValue = useMemo(() => { diff --git a/packages/ui/QuestionFormInput/index.tsx b/packages/ui/QuestionFormInput/index.tsx index 2c84cb6f0b..274059bdda 100644 --- a/packages/ui/QuestionFormInput/index.tsx +++ b/packages/ui/QuestionFormInput/index.tsx @@ -120,7 +120,7 @@ export const QuestionFormInput = ({ [value, id, isInvalid, surveyLanguageCodes] ); - const getElementTextBasedOnType = useCallback((): TI18nString => { + const elementText = useMemo((): TI18nString => { if (isChoice && typeof index === "number") { return getChoiceLabel(question, index, surveyLanguageCodes); } @@ -155,8 +155,7 @@ export const QuestionFormInput = ({ surveyLanguageCodes, ]); - const [text, setText] = useState(getElementTextBasedOnType()); - // const [debouncedText, setDebouncedText] = useState(text); // Added debouncedText state + const [text, setText] = useState(elementText); const [renderedText, setRenderedText] = useState(); const [showImageUploader, setShowImageUploader] = useState( determineImageUploaderVisibility(questionIdx, localSurvey) @@ -173,11 +172,11 @@ export const QuestionFormInput = ({ ) : [] ); - const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>( - getLocalizedValue(text, usedLanguageCode).includes("/fallback:") - ? getFallbackValues(getLocalizedValue(text, usedLanguageCode)) - : {} - ); + + const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>(() => { + const localizedValue = getLocalizedValue(text, usedLanguageCode); + return localizedValue.includes("/fallback:") ? getFallbackValues(localizedValue) : {}; + }); const highlightContainerRef = useRef(null); const fallbackInputRef = useRef(null); @@ -204,9 +203,9 @@ export const QuestionFormInput = ({ }, [usedLanguageCode]); useEffect(() => { - if (id === "headline" || id === "subheader") { - checkForRecallSymbol(); - } + // if (id === "headline" || id === "subheader") { + // checkForRecallSymbol(); + // } // Generates an array of headlines from recallItems, replacing nested recall questions with '___' . const recallItemLabels = recallItems.flatMap((recallItem) => { if (!recallItem.label.includes("#recall:")) { @@ -262,6 +261,7 @@ export const QuestionFormInput = ({ } return parts; }; + setRenderedText(processInput()); }, [text, recallItems]); @@ -271,100 +271,21 @@ export const QuestionFormInput = ({ } }, [showFallbackInput]); - useEffect(() => { - setText(getElementTextBasedOnType()); - }, [localSurvey]); + // useEffect(() => { + // setText(getElementTextBasedOnType()); + // }, [localSurvey]); - const checkForRecallSymbol = () => { - const pattern = /(^|\s)@(\s|$)/; - if (pattern.test(getLocalizedValue(text, usedLanguageCode))) { - setShowRecallItemSelect(true); - } else { - setShowRecallItemSelect(false); - } - }; - - // Adds a new recall question to the recallItems array, updates fallbacks, modifies the text with recall details. - const addRecallItem = (recallItem: TSurveyRecallItem) => { - if (recallItem.label.trim() === "") { - toast.error("Cannot add question with empty headline as recall"); - return; - } - let recallItemTemp = structuredClone(recallItem); - recallItemTemp.label = replaceRecallInfoWithUnderline(recallItem.label); - setRecallItems((prevQuestions) => { - const updatedQuestions = [...prevQuestions, recallItemTemp]; - return updatedQuestions; - }); - if (!Object.keys(fallbacks).includes(recallItem.id)) { - setFallbacks((prevFallbacks) => ({ - ...prevFallbacks, - [recallItem.id]: "", - })); - } - setShowRecallItemSelect(false); - let modifiedHeadlineWithId = { ...getElementTextBasedOnType() }; - modifiedHeadlineWithId[usedLanguageCode] = getLocalizedValue( - modifiedHeadlineWithId, - usedLanguageCode - ).replace(/(?<=^|\s)@(?=\s|$)/g, `#recall:${recallItem.id}/fallback:# `); - handleUpdate(getLocalizedValue(modifiedHeadlineWithId, usedLanguageCode)); - const modifiedHeadlineWithName = recallToHeadline( - modifiedHeadlineWithId, - localSurvey, - false, - usedLanguageCode, - attributeClasses - ); - 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 filterRecallItems = (remainingText: string) => { - let includedRecallItems: TSurveyRecallItem[] = []; - recallItems.forEach((recallItem) => { - if (remainingText.includes(`@${recallItem.label}`)) { - includedRecallItems.push(recallItem); + const checkForRecallSymbol = useCallback( + (value: TI18nString) => { + const pattern = /(^|\s)@(\s|$)/; + if (pattern.test(getLocalizedValue(value, usedLanguageCode))) { + setShowRecallItemSelect(true); } else { - const recallItemToRemove = recallItem.label.slice(0, -1); - const newText = { ...text }; - newText[usedLanguageCode] = text[usedLanguageCode].replace(`@${recallItemToRemove}`, ""); - setText(newText); - handleUpdate(text[usedLanguageCode].replace(`@${recallItemToRemove}`, "")); - let updatedFallback = { ...fallbacks }; - delete updatedFallback[recallItem.id]; - setFallbacks(updatedFallback); - setRecallItems(includedRecallItems); + setShowRecallItemSelect(false); } - }); - }; - - const addFallback = () => { - let headlineWithFallback = getElementTextBasedOnType(); - filteredRecallItems.forEach((recallQuestion) => { - if (recallQuestion) { - const recallInfo = findRecallInfoById( - getLocalizedValue(headlineWithFallback, usedLanguageCode), - 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[usedLanguageCode] = getLocalizedValue( - headlineWithFallback, - usedLanguageCode - ).replace(recallInfo, `#recall:${recallQuestion?.id}/fallback:${fallBackValue}#`); - handleUpdate(getLocalizedValue(headlineWithFallback, usedLanguageCode)); - } - } - }); - setShowFallbackInput(false); - inputRef.current?.focus(); - }; + }, + [usedLanguageCode] + ); // updation of questions, WelcomeCard, ThankYouCard and choices is done in a different manner, // questions -> updateQuestion @@ -375,11 +296,11 @@ export const QuestionFormInput = ({ const createUpdatedText = useCallback( (updatedText: string): TI18nString => { return { - ...getElementTextBasedOnType(), + ...elementText, [usedLanguageCode]: updatedText, }; }, - [getElementTextBasedOnType, usedLanguageCode] + [elementText, usedLanguageCode] ); const updateChoiceDetails = useCallback( @@ -446,6 +367,103 @@ export const QuestionFormInput = ({ ] ); + // Adds a new recall question to the recallItems array, updates fallbacks, modifies the text with recall details. + const addRecallItem = useCallback( + (recallItem: TSurveyRecallItem) => { + if (recallItem.label.trim() === "") { + toast.error("Cannot add question with empty headline as recall"); + return; + } + + let recallItemTemp = structuredClone(recallItem); + recallItemTemp.label = replaceRecallInfoWithUnderline(recallItem.label); + + setRecallItems((prevQuestions) => { + const updatedQuestions = [...prevQuestions, recallItemTemp]; + return updatedQuestions; + }); + + if (!Object.keys(fallbacks).includes(recallItem.id)) { + setFallbacks((prevFallbacks) => ({ + ...prevFallbacks, + [recallItem.id]: "", + })); + } + + setShowRecallItemSelect(false); + + let modifiedHeadlineWithId = { ...elementText }; + modifiedHeadlineWithId[usedLanguageCode] = getLocalizedValue( + modifiedHeadlineWithId, + usedLanguageCode + ).replace(/(?<=^|\s)@(?=\s|$)/g, `#recall:${recallItem.id}/fallback:# `); + + handleUpdate(getLocalizedValue(modifiedHeadlineWithId, usedLanguageCode)); + + const modifiedHeadlineWithName = recallToHeadline( + modifiedHeadlineWithId, + localSurvey, + false, + usedLanguageCode, + attributeClasses + ); + + setText(modifiedHeadlineWithName); + setShowFallbackInput(true); + }, + [attributeClasses, elementText, fallbacks, handleUpdate, localSurvey, usedLanguageCode] + ); + + // Filters and updates the list of recall questions based on their presence in the given text, also managing related text and fallback states. + const filterRecallItems = useCallback( + (remainingText: string) => { + let includedRecallItems: TSurveyRecallItem[] = []; + + recallItems.forEach((recallItem) => { + if (remainingText.includes(`@${recallItem.label}`)) { + includedRecallItems.push(recallItem); + } else { + const recallItemToRemove = recallItem.label.slice(0, -1); + const newText = { ...text }; + newText[usedLanguageCode] = text[usedLanguageCode].replace(`@${recallItemToRemove}`, ""); + setText(newText); + handleUpdate(text[usedLanguageCode].replace(`@${recallItemToRemove}`, "")); + let updatedFallback = { ...fallbacks }; + delete updatedFallback[recallItem.id]; + setFallbacks(updatedFallback); + setRecallItems(includedRecallItems); + } + }); + }, + [fallbacks, handleUpdate, recallItems, text, usedLanguageCode] + ); + + const addFallback = () => { + let headlineWithFallback = elementText; + filteredRecallItems.forEach((recallQuestion) => { + if (recallQuestion) { + const recallInfo = findRecallInfoById( + getLocalizedValue(headlineWithFallback, usedLanguageCode), + 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[usedLanguageCode] = getLocalizedValue( + headlineWithFallback, + usedLanguageCode + ).replace(recallInfo, `#recall:${recallQuestion?.id}/fallback:${fallBackValue}#`); + handleUpdate(getLocalizedValue(headlineWithFallback, usedLanguageCode)); + } + } + }); + setShowFallbackInput(false); + inputRef.current?.focus(); + }; + const getFileUrl = (): string | undefined => { if (isWelcomeCard) return localSurvey.welcomeCard.fileUrl; if (isEndingCard) { @@ -470,16 +488,29 @@ export const QuestionFormInput = ({ const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; const updatedText = { - ...getElementTextBasedOnType(), + ...elementText, [usedLanguageCode]: value, }; - setText(recallToHeadline(updatedText, localSurvey, false, usedLanguageCode, attributeClasses)); - if (!isE2E) { - debouncedHandleUpdate(value); - } else { - handleUpdate(headlineToRecall(value, recallItems, fallbacks)); + const valueTI18nString = recallToHeadline( + updatedText, + localSurvey, + false, + usedLanguageCode, + attributeClasses + ); + + setText(valueTI18nString); + + if (id === "headline" || id === "subheader") { + checkForRecallSymbol(valueTI18nString); } + + // if (!isE2E) { + // debouncedHandleUpdate(value); + // } else { + handleUpdate(headlineToRecall(value, recallItems, fallbacks)); + // } }; return ( @@ -525,7 +556,7 @@ export const QuestionFormInput = ({ dir="auto"> {renderedText} - {getLocalizedValue(getElementTextBasedOnType(), usedLanguageCode).includes("recall:") && ( + {getLocalizedValue(elementText, usedLanguageCode).includes("recall:") && (