mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
fix: question, hiddenfield, variable delete error if used in logic
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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}`)) {
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user