fix: testing perf

This commit is contained in:
pandeymangg
2024-09-26 15:00:40 +05:30
parent 2f63659739
commit 3db7c7946b
10 changed files with 569 additions and 367 deletions

View File

@@ -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({
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
{question.logic && question.logic.length > 0 && (
{questionLogic && questionLogic.length > 0 && (
<div className="mt-2 flex flex-col gap-4">
{question.logic.map((logicItem, logicItemIdx) => (
{questionLogic.map((logicItem, logicItemIdx) => (
<div
key={logicItem.id}
className="flex w-full grow items-start gap-2 rounded-lg border border-slate-200 bg-slate-50 p-4">
<LogicEditor
localSurvey={transformedSurvey}
logicItem={logicItem}
updateQuestion={updateQuestion}
updateQuestion={updateQuestionLogic}
question={question}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
isLast={logicItemIdx === (question.logic ?? []).length - 1}
isLast={logicItemIdx === (questionLogic ?? []).length - 1}
/>
<DropdownMenu>
@@ -159,7 +171,7 @@ export function ConditionalLogic({
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={logicItemIdx === (question.logic ?? []).length - 1}
disabled={logicItemIdx === (questionLogic ?? []).length - 1}
onClick={() => {
moveLogic(logicItemIdx, logicItemIdx + 1);
}}>

View File

@@ -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 (
<div key={action.id} className="flex grow items-center justify-between gap-x-2">
<div className="block w-9 shrink-0">{actionIdx === 0 ? "Then" : "and"}</div>
<div className="flex grow items-center gap-x-2">
<InputCombobox
id={`action-${actionIdx}-objective`}
key={`objective-${action.id}`}
showSearch={false}
options={actionObjectiveOptions}
value={action.objective}
onChangeValue={(val: TActionObjective) => {
handleObjectiveChange(actionIdx, val);
}}
comboboxClasses="grow"
/>
{action.objective !== "calculate" && (
<InputCombobox
id={`action-${actionIdx}-target`}
key={`target-${action.id}`}
showSearch={false}
options={actionTargetOptions}
value={action.target}
onChangeValue={(val: string) => {
handleValuesChange(actionIdx, {
target: val,
});
}}
comboboxClasses="grow"
/>
)}
{/* {action.objective === "calculate" && (
<>
<InputCombobox
id={`action-${actionIdx}-variableId`}
key={`variableId-${action.id}`}
showSearch={false}
options={getActionVariableOptions(localSurvey)}
value={action.variableId}
onChangeValue={(val: string) => {
handleValuesChange(actionIdx, {
variableId: val,
value: {
type: "static",
value: "",
},
});
}}
comboboxClasses="grow"
emptyDropdownText="Add a variable to calculate"
/>
<InputCombobox
id={`action-${actionIdx}-operator`}
key={`operator-${action.id}`}
showSearch={false}
options={getActionOperatorOptions(
localSurvey.variables.find((v) => v.id === action.variableId)?.type
)}
value={action.operator}
onChangeValue={(
val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator
) => {
handleValuesChange(actionIdx, {
operator: val,
});
}}
comboboxClasses="grow"
/>
<InputCombobox
id={`action-${actionIdx}-value`}
key={`value-${action.id}`}
withInput={true}
clearable={true}
value={action.value?.value ?? ""}
inputProps={{
placeholder: "Value",
type: localSurvey.variables.find((v) => 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"
/>
</>
)} */}
</div>
<DropdownMenu>
<DropdownMenuTrigger id={`actions-${actionIdx}-dropdown`}>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("addBelow", actionIdx);
}}>
<PlusIcon className="h-4 w-4" />
Add action below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={isRemoveDisabled}
onClick={() => {
handleActionsChange("remove", actionIdx);
}}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("duplicate", actionIdx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export const LogicEditorAction = React.memo(_LogicEditorAction);

View File

@@ -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<TSurveyLogicAction>) => {
const action = actions[actionIdx];
const actionBody = { ...action, ...values } as TSurveyLogicAction;
handleActionsChange("update", actionIdx, actionBody);
};
const handleValuesChange = useCallback(
(actionIdx: number, values: Partial<TSurveyLogicAction>) => {
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 (
<div className="flex grow gap-2">
<CornerDownRightIcon className="mt-3 h-4 w-4 shrink-0" />
<div className="flex grow flex-col gap-y-2">
{actions?.map((action, idx) => (
<div key={action.id} className="flex grow items-center justify-between gap-x-2">
<div className="block w-9 shrink-0">{idx === 0 ? "Then" : "and"}</div>
<div className="flex grow items-center gap-x-2">
<InputCombobox
id={`action-${idx}-objective`}
key={`objective-${action.id}`}
showSearch={false}
options={actionObjectiveOptions}
value={action.objective}
onChangeValue={(val: TActionObjective) => {
handleObjectiveChange(idx, val);
}}
comboboxClasses="grow"
/>
{action.objective !== "calculate" && (
<InputCombobox
id={`action-${idx}-target`}
key={`target-${action.id}`}
showSearch={false}
options={getActionTargetOptions(action, localSurvey, questionIdx)}
value={action.target}
onChangeValue={(val: string) => {
handleValuesChange(idx, {
target: val,
});
}}
comboboxClasses="grow"
/>
)}
{action.objective === "calculate" && (
<>
<InputCombobox
id={`action-${idx}-variableId`}
key={`variableId-${action.id}`}
showSearch={false}
options={getActionVariableOptions(localSurvey)}
value={action.variableId}
onChangeValue={(val: string) => {
handleValuesChange(idx, {
variableId: val,
value: {
type: "static",
value: "",
},
});
}}
comboboxClasses="grow"
emptyDropdownText="Add a variable to calculate"
/>
<InputCombobox
id={`action-${idx}-operator`}
key={`operator-${action.id}`}
showSearch={false}
options={getActionOperatorOptions(
localSurvey.variables.find((v) => v.id === action.variableId)?.type
)}
value={action.operator}
onChangeValue={(
val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator
) => {
handleValuesChange(idx, {
operator: val,
});
}}
comboboxClasses="grow"
/>
<InputCombobox
id={`action-${idx}-value`}
key={`value-${action.id}`}
withInput={true}
clearable={true}
value={action.value?.value ?? ""}
inputProps={{
placeholder: "Value",
type: localSurvey.variables.find((v) => 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"
/>
</>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger id={`actions-${idx}-dropdown`}>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("addBelow", idx);
}}>
<PlusIcon className="h-4 w-4" />
Add action below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={actions.length === 1}
onClick={() => {
handleActionsChange("remove", idx);
}}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("duplicate", idx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<LogicEditorAction
action={action}
actionIdx={idx}
handleActionsChange={handleActionsChange}
handleObjectiveChange={handleObjectiveChange}
handleValuesChange={handleValuesChange}
endings={endings}
isRemoveDisabled={actions.length === 1}
filteredQuestions={filteredQuestions}
/>
))}
</div>
</div>
);
}
};
// 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);
};

View File

@@ -258,7 +258,7 @@ export function LogicEditorConditions({
</div>
)}
</div>
<InputCombobox
{/* <InputCombobox
id={`condition-${depth}-${index}-conditionValue`}
key="conditionValue"
showSearch={false}
@@ -300,7 +300,7 @@ export function LogicEditorConditions({
handleRightOperandChange(condition, val, option);
}}
/>
)}
)} */}
<DropdownMenu>
<DropdownMenuTrigger id={`condition-${depth}-${index}-dropdown`}>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />

View File

@@ -264,6 +264,7 @@ export const QuestionsView = ({
}
}
});
setLocalSurvey(updatedSurvey);
validateSurveyQuestion(updatedSurvey.questions[questionIdx]);
};

View File

@@ -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 ?? [];

View File

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

View File

@@ -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]}#`;

View File

@@ -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(() => {

View File

@@ -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<JSX.Element[]>();
const [showImageUploader, setShowImageUploader] = useState<boolean>(
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<HTMLInputElement>(null);
const fallbackInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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}
</div>
{getLocalizedValue(getElementTextBasedOnType(), usedLanguageCode).includes("recall:") && (
{getLocalizedValue(elementText, usedLanguageCode).includes("recall:") && (
<button
className="fixed right-14 hidden items-center rounded-b-lg bg-slate-100 px-2.5 py-1 text-xs hover:bg-slate-200 group-hover:flex"
onClick={(e) => {