fix: question, hiddenfield, variable delete error if used in logic

This commit is contained in:
Piyush Gupta
2024-08-28 16:35:05 +05:30
parent c32ced20f1
commit d56f05fb19
9 changed files with 347 additions and 131 deletions

View File

@@ -107,7 +107,7 @@ export function AdvancedLogicEditorActions({
options={
action.objective === "calculate"
? getActionVariableOptions(localSurvey)
: getActionTargetOptions(localSurvey, questionIdx)
: getActionTargetOptions(action, localSurvey, questionIdx)
}
selected={action.objective === "calculate" ? action.variableId : action.target}
onChangeValue={(val: string) => {

View File

@@ -1,5 +1,6 @@
"use client";
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { toast } from "react-hot-toast";
@@ -64,6 +65,25 @@ export const HiddenFieldsCard = ({
});
};
const handleDeleteHiddenField = (fieldId: string) => {
const quesIdx = findHiddenFieldUsedInLogic(localSurvey, fieldId);
if (quesIdx !== -1) {
toast.error(
`${fieldId} is used in logic of question ${quesIdx + 1}. Please remove it from logic first.`
);
return;
}
updateSurvey(
{
enabled: true,
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
},
fieldId
);
};
return (
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
<div
@@ -110,15 +130,7 @@ export const HiddenFieldsCard = ({
return (
<Tag
key={fieldId}
onDelete={() => {
updateSurvey(
{
enabled: true,
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
},
fieldId
);
}}
onDelete={(fieldId) => handleDeleteHiddenField(fieldId)}
tagId={fieldId}
tagName={fieldId}
/>

View File

@@ -1,10 +1,12 @@
"use client";
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
@@ -68,8 +70,6 @@ export const MultipleChoiceQuestionForm = ({
};
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
// const newLabel = updatedAttributes.label.en;
// const oldLabel = question.choices[choiceIdx].label;
let newChoices: any[] = [];
if (question.choices) {
newChoices = question.choices.map((choice, idx) => {
@@ -78,21 +78,8 @@ export const MultipleChoiceQuestionForm = ({
});
}
// let newLogic: any[] = [];
// question.logic?.forEach((logic) => {
// let newL: string | string[] | undefined = logic.value;
// if (Array.isArray(logic.value)) {
// newL = logic.value.map((value) =>
// value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : value
// );
// } else {
// newL = logic.value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : logic.value;
// }
// newLogic.push({ ...logic, value: newL });
// });
updateQuestion(questionIdx, {
choices: newChoices,
// logic: newLogic
});
};
@@ -135,25 +122,24 @@ export const MultipleChoiceQuestionForm = ({
};
const deleteChoice = (choiceIdx: number) => {
const choiceToDelete = question.choices[choiceIdx].id;
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, choiceToDelete);
if (questionIdx !== -1) {
toast.error(
`This option is used in logic for question ${questionIdx + 1}. Please fix the logic first before deleting.`
);
return;
}
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
if (isInvalidValue === choiceValue) {
setisInvalidValue(null);
}
// let newLogic: any[] = [];
// question.logic?.forEach((logic) => {
// let newL: string | string[] | undefined = logic.value;
// if (Array.isArray(logic.value)) {
// newL = logic.value.filter((value) => value !== choiceValue);
// } else {
// newL = logic.value !== choiceValue ? logic.value : undefined;
// }
// newLogic.push({ ...logic, value: newL });
// });
updateQuestion(questionIdx, {
choices: newChoices,
// logic: newLogic
});
};

View File

@@ -2,6 +2,7 @@
import { AddEndingCardButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddEndingCardButton";
import { SurveyVariablesCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard";
import { findQuestionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import {
DndContext,
DragEndEvent,
@@ -17,11 +18,18 @@ import toast from "react-hot-toast";
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import {
TAction,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogic,
} from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation";
import {
@@ -76,22 +84,75 @@ export const QuestionsView = ({
const surveyLanguages = localSurvey.languages;
const [backButtonLabel, setbackButtonLabel] = useState(null);
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
survey.questions.forEach((question) => {
if (question.headline[selectedLanguageCode].includes(`recall:${compareId}`)) {
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replaceAll(
`recall:${compareId}`,
`recall:${updatedId}`
);
const updateConditions = (conditions: TConditionGroup): TConditionGroup => {
return {
...conditions,
conditions: conditions.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
return updateConditions(condition);
} else {
return updateSingleCondition(condition);
}
}),
};
};
const updateSingleCondition = (condition: TSingleCondition): TSingleCondition => {
let updatedCondition = { ...condition };
if (condition.leftOperand.id === compareId) {
updatedCondition.leftOperand = { ...condition.leftOperand, id: updatedId };
}
if (!question.logic) return;
question.logic.forEach((rule) => {
if (rule.destination === compareId) {
rule.destination = updatedId;
if (condition.rightOperand?.type === "question" && condition.rightOperand.value === compareId) {
updatedCondition.rightOperand = { ...condition.rightOperand, value: updatedId };
}
return updatedCondition;
};
const updateActions = (actions: TAction[]): TAction[] => {
return actions.map((action) => {
let updatedAction = { ...action };
if (updatedAction.objective === "jumpToQuestion" && updatedAction.target === compareId) {
updatedAction.target = updatedId;
}
if (updatedAction.objective === "requireAnswer" && updatedAction.target === compareId) {
updatedAction.target = updatedId;
}
return updatedAction;
});
});
return survey;
};
return {
...survey,
questions: survey.questions.map((question) => {
let updatedQuestion = { ...question };
if (question.headline[selectedLanguageCode].includes(`recall:${compareId}`)) {
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replaceAll(
`recall:${compareId}`,
`recall:${updatedId}`
);
}
// Update advanced logic
if (question.logic) {
updatedQuestion.logic = question.logic.map((logicRule: TSurveyAdvancedLogic) => ({
...logicRule,
conditions: updateConditions(logicRule.conditions),
actions: updateActions(logicRule.actions),
}));
}
return updatedQuestion;
}),
};
};
useEffect(() => {
@@ -210,6 +271,16 @@ export const QuestionsView = ({
const activeQuestionIdTemp = activeQuestionId ?? localSurvey.questions[0].id;
let updatedSurvey: TSurvey = { ...localSurvey };
// checking if this question is used in logic of any other question
const quesIdx = findQuestionUsedInLogic(localSurvey, questionId);
if (quesIdx !== -1) {
toast.error(
`This question is used in logic of question ${quesIdx + 1}. ${localSurvey.questions[quesIdx].headline["default"]}`
);
return;
}
// check if we are recalling from this question for every language
updatedSurvey.questions.forEach((question) => {
for (const [languageCode, headline] of Object.entries(question.headline)) {
@@ -222,7 +293,7 @@ export const QuestionsView = ({
}
});
updatedSurvey.questions.splice(questionIdx, 1);
updatedSurvey = handleQuestionLogicChange(updatedSurvey, questionId, "");
const firstEndingCard = localSurvey.endings[0];
setLocalSurvey(updatedSurvey);
delete internalQuestionIdMap[questionId];

View File

@@ -1,9 +1,11 @@
"use client";
import { findVariableUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import { createId } from "@paralleldrive/cuid2";
import { TrashIcon } from "lucide-react";
import React, { useCallback, useEffect } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/Button";
@@ -75,8 +77,16 @@ export const SurveyVariablesCardItem = ({
const onVaribleDelete = (variable: TSurveyVariable) => {
const questions = [...localSurvey.questions];
// find if this variable is used in any question's recall and remove it for every language
const quesIdx = findVariableUsedInLogic(localSurvey, variable.id);
if (quesIdx !== -1) {
toast.error(
`${variable.name} is used in logic of question ${quesIdx + 1}. Please remove it from logic first.`
);
return;
}
// find if this variable is used in any question's recall and remove it for every language
questions.forEach((question) => {
for (const [languageCode, headline] of Object.entries(question.headline)) {
if (headline.includes(`recall:${variable.id}`)) {

View File

@@ -18,10 +18,16 @@ import {
StarIcon,
} from "lucide-react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import {
TAction,
TActionObjective,
TActionVariableCalculateOperator,
TConditionGroup,
TLeftOperand,
TRightOperand,
TSingleCondition,
TSurveyAdvancedLogic,
} from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestionTypeEnum, TSurveyVariable } from "@formbricks/types/surveys/types";
import { ComboboxGroupedOption, ComboboxOption } from "@formbricks/ui/InputCombobox";
@@ -336,7 +342,11 @@ export const getMatchValueProps = (
return { show: true, options: [] };
};
export const getActionTargetOptions = (localSurvey: TSurvey, currQuestionIdx: number): ComboboxOption[] => {
export const getActionTargetOptions = (
action: TAction,
localSurvey: TSurvey,
currQuestionIdx: number
): ComboboxOption[] => {
const questionOptions = localSurvey.questions
.filter((_, idx) => idx !== currQuestionIdx)
.map((question) => {
@@ -347,11 +357,13 @@ export const getActionTargetOptions = (localSurvey: TSurvey, currQuestionIdx: nu
};
});
if (action.objective === "requireAnswer") return questionOptions;
const endingCardOptions = localSurvey.endings.map((ending) => {
return {
label:
ending.type === "endScreen"
? `🙏${getLocalizedValue(ending.headline, "default")}`
? `🙏 ${getLocalizedValue(ending.headline, "default")}`
: `🙏 ${ending.label || "Redirect Thank you card"}`,
value: ending.id,
};
@@ -492,3 +504,134 @@ export const getActionValueOptions = (
return groupedOptions;
};
export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, questionId)) ||
isUsedInLeftOperand(condition.leftOperand, questionId)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand, id: string): boolean => {
return leftOperand.type === "question" && leftOperand.id === id;
};
const isUsedInRightOperand = (rightOperand: TRightOperand, id: string): boolean => {
return rightOperand.type === "question" && rightOperand.value === id;
};
const isUsedInAction = (action: TAction): boolean => {
return (
(action.objective === "jumpToQuestion" && action.target === questionId) ||
(action.objective === "requireAnswer" && action.target === questionId)
);
};
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
};
return survey.questions
.filter((question) => question.id !== questionId)
.findIndex((question) => question.logic && question.logic.some(isUsedInLogicRule));
};
export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optionId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return isUsedInOperand(condition);
}
};
const isUsedInOperand = (condition: TSingleCondition): boolean => {
if (condition.leftOperand.type === "question" && condition.leftOperand.id === questionId) {
if (condition.rightOperand && condition.rightOperand.type === "static") {
if (Array.isArray(condition.rightOperand.value)) {
return condition.rightOperand.value.includes(optionId);
} else {
return condition.rightOperand.value === optionId;
}
}
}
return false;
};
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
return isUsedInCondition(logicRule.conditions);
};
return survey.questions.findIndex((question) => question.logic && question.logic.some(isUsedInLogicRule));
};
export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand)) ||
isUsedInLeftOperand(condition.leftOperand)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand): boolean => {
return leftOperand.type === "variable" && leftOperand.id === variableId;
};
const isUsedInRightOperand = (rightOperand: TRightOperand): boolean => {
return rightOperand.type === "variable" && rightOperand.value === variableId;
};
const isUsedInAction = (action: TAction): boolean => {
return action.objective === "calculate" && action.variableId === variableId;
};
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
};
return survey.questions.findIndex((question) => question.logic && question.logic.some(isUsedInLogicRule));
};
export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand)) ||
isUsedInLeftOperand(condition.leftOperand)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand): boolean => {
return leftOperand.type === "hiddenField" && leftOperand.id === hiddenFieldId;
};
const isUsedInRightOperand = (rightOperand: TRightOperand): boolean => {
return rightOperand.type === "hiddenField" && rightOperand.value === hiddenFieldId;
};
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
return isUsedInCondition(logicRule.conditions);
};
return survey.questions.findIndex((question) => question.logic && question.logic.some(isUsedInLogicRule));
};

View File

@@ -5,53 +5,6 @@ import { env } from "@formbricks/lib/env";
export const formbricksEnabled =
typeof env.NEXT_PUBLIC_FORMBRICKS_API_HOST && env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID;
// !@gupta-piyush19, @mattinannt : can we remove this code?
// const ttc = { onboarding: 0 };
// const getFormbricksApi = () => {
// const environmentId = env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID;
// const apiHost = env.NEXT_PUBLIC_FORMBRICKS_API_HOST;
// if (typeof environmentId !== "string" || typeof apiHost !== "string") {
// throw new Error("Formbricks environment ID or API host is not defined");
// }
// return new FormbricksAPI({
// environmentId,
// apiHost,
// });
// };
// export const createResponse = async (
// surveyId: string,
// userId: string,
// data: { [questionId: string]: any },
// finished: boolean = false
// ): Promise<any> => {
// const api = getFormbricksApi();
// return await api.client.response.create({
// surveyId,
// userId,
// finished,
// data,
// ttc,
// });
// };
// export const updateResponse = async (
// responseId: string,
// data: { [questionId: string]: any },
// finished: boolean = false
// ): Promise<any> => {
// const api = getFormbricksApi();
// return await api.client.response.update({
// responseId,
// finished,
// data,
// ttc,
// });
// };
export const formbricksLogout = async () => {
return await formbricks.logout();
};

View File

@@ -19,7 +19,12 @@ import type {
TResponseTtc,
TResponseVariables,
} from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { TSurvey } from "@formbricks/types/surveys/types";
interface VariableStackEntry {
questionId: string;
variables: TResponseVariables;
}
export const Survey = ({
survey,
@@ -68,7 +73,13 @@ export const Survey = ({
const [loadingElement, setLoadingElement] = useState(false);
const [history, setHistory] = useState<string[]>([]);
const [responseData, setResponseData] = useState<TResponseData>(hiddenFieldsRecord ?? {});
const [responseVariables, setResponseVariables] = useState<TResponseVariables>({});
const [variableStack, setVariableStack] = useState<VariableStackEntry[]>([]);
const [currentVariables, setCurrentVariables] = useState<TResponseVariables>(() => {
return localSurvey.variables.reduce((acc, variable) => {
acc[variable.id] = variable.value;
return acc;
}, {} as TResponseVariables);
});
const [ttc, setTtc] = useState<TResponseTtc>({});
const questionIds = useMemo(
@@ -154,8 +165,8 @@ export const Survey = ({
};
const onChangeVariables = (variables: TResponseVariables) => {
const updatedVariables = { ...responseVariables, ...variables };
setResponseVariables(updatedVariables);
const updatedVariables = { ...currentVariables, ...variables };
setCurrentVariables(updatedVariables);
};
const makeQuestionsRequired = (questionIds: string[]): void => {
@@ -169,30 +180,45 @@ export const Survey = ({
setlocalSurvey(localSurveyClone);
};
const getNextQuestionId = (
survey: { questions: TSurveyQuestion[]; endings: { id: string }[] },
questionId: string,
data: TResponseData,
selectedLanguage: string
): string | undefined => {
const pushVariableState = (questionId: string) => {
setVariableStack((prevStack) => [...prevStack, { questionId, variables: { ...currentVariables } }]);
};
const popVariableState = () => {
setVariableStack(() => {
const newStack = [...variableStack];
const poppedState = newStack.pop();
if (poppedState) {
setCurrentVariables(poppedState.variables);
}
return newStack;
});
};
const evaluateLogicAndGetNextQuestionId = (
data: TResponseData
): { nextQuestionId: string | undefined; calculatedVariables: TResponseVariables } => {
const questions = survey.questions;
const firstEndingId = survey.endings.length > 0 ? survey.endings[0].id : undefined;
if (questionId === "start") return questions[0]?.id || firstEndingId;
if (questionId === "start")
return { nextQuestionId: questions[0]?.id || firstEndingId, calculatedVariables: {} };
if (currIdxTemp === -1) throw new Error("Question not found");
if (!currQuesTemp) throw new Error("Question not found");
let firstJumpTarget: string | undefined;
const allRequiredQuestionIds: string[] = [];
const calculationResults: Record<string, number | string> = {};
if (currQuesTemp?.logic && currQuesTemp.logic.length > 0) {
let calculationResults = { ...currentVariables };
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
for (const logic of currQuesTemp.logic) {
if (evaluateAdvancedLogic(localSurvey, data, logic.conditions, selectedLanguage)) {
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
localSurvey,
logic.actions,
data
data,
calculationResults
);
if (jumpTarget && !firstJumpTarget) {
@@ -200,36 +226,40 @@ export const Survey = ({
}
allRequiredQuestionIds.push(...requiredQuestionIds);
Object.assign(calculationResults, calculations);
calculationResults = { ...calculationResults, ...calculations };
}
}
}
// Update response variables with all calculation results
onChangeVariables(calculationResults);
// Make all collected questions required
if (allRequiredQuestionIds.length > 0) {
makeQuestionsRequired(allRequiredQuestionIds);
}
// Return the first jump target if found, otherwise go to the next question or ending
return firstJumpTarget || questions[currentQuestionIndex + 1]?.id || firstEndingId;
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || firstEndingId;
return { nextQuestionId, calculatedVariables: calculationResults };
};
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc) => {
const questionId = Object.keys(responseData)[0];
setLoadingElement(true);
const nextQuestionId = getNextQuestionId(localSurvey, questionId, responseData, selectedLanguage);
pushVariableState(questionId);
const { nextQuestionId, calculatedVariables } = evaluateLogicAndGetNextQuestionId(responseData);
const finished =
nextQuestionId === undefined ||
!localSurvey.questions.map((question) => question.id).includes(nextQuestionId);
onChange(responseData);
onChangeVariables(calculatedVariables);
onResponse({
data: responseData,
ttc,
finished,
variables: responseVariables,
variables: calculatedVariables,
language: selectedLanguage,
});
if (finished) {
@@ -256,6 +286,7 @@ export const Survey = ({
// otherwise go back to previous question in array
prevQuestionId = localSurvey.questions[currIdxTemp - 1]?.id;
}
popVariableState();
if (!prevQuestionId) throw new Error("Question not found");
setQuestionId(prevQuestionId);
};

View File

@@ -1,6 +1,6 @@
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import { TResponseData } from "@formbricks/types/responses";
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import {
TAction,
TActionCalculate,
@@ -211,21 +211,22 @@ const getRightOperandValue = (
export const performActions = (
survey: TSurvey,
actions: TAction[],
data: TResponseData
data: TResponseData,
calculationResults: TResponseVariables
): {
jumpTarget: string | undefined;
requiredQuestionIds: string[];
calculations: Record<string, number | string>;
calculations: TResponseVariables;
} => {
let jumpTarget: string | undefined;
const requiredQuestionIds: string[] = [];
const calculations: Record<string, number | string> = {};
const calculations: TResponseVariables = { ...calculationResults };
actions.forEach((action) => {
switch (action.objective) {
case "calculate":
const result = performCalculation(survey, action, data);
if (result) calculations[action.variableId] = result;
const result = performCalculation(survey, action, data, calculations);
if (result !== undefined) calculations[action.variableId] = result;
break;
case "requireAnswer":
requiredQuestionIds.push(action.target);
@@ -244,14 +245,18 @@ export const performActions = (
const performCalculation = (
survey: TSurvey,
action: TActionCalculate,
data: TResponseData
data: TResponseData,
calculations: Record<string, number | string>
): number | string | undefined => {
const variables = survey.variables || [];
const variable = variables.find((v) => v.id === action.variableId);
if (!variable) return undefined;
let currentValue = data[action.variableId] || variable.type === "number" ? 0 : "";
let currentValue = calculations[action.variableId];
if (currentValue === undefined) {
currentValue = variable.type === "number" ? 0 : "";
}
let operandValue: string | number | undefined;
// Determine the operand value based on the action.value type
@@ -259,8 +264,13 @@ const performCalculation = (
case "static":
operandValue = action.value.value;
break;
case "question":
case "variable":
const value = calculations[action.value.value];
if (typeof value === "number" || typeof value === "string") {
operandValue = value;
}
break;
case "question":
case "hiddenField":
const val = data[action.value.value];
if (typeof val === "number" || typeof val === "string") {
@@ -272,7 +282,7 @@ const performCalculation = (
break;
}
if (!operandValue) return undefined;
if (operandValue === undefined || operandValue === null) return undefined;
let result: number | string;