mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
fix: dropoff and comparison funciton
This commit is contained in:
@@ -209,16 +209,24 @@ export const getMatchValueProps = (
|
||||
|
||||
if (condition.leftOperand.type === "question") {
|
||||
if (selectedQuestion?.type === TSurveyQuestionTypeEnum.OpenText) {
|
||||
const allowedQuestions = questions.filter((question) =>
|
||||
[
|
||||
const allowedQuestions = questions.filter((question) => {
|
||||
const allowedQuestionTypes = [
|
||||
TSurveyQuestionTypeEnum.OpenText,
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyQuestionTypeEnum.Rating,
|
||||
TSurveyQuestionTypeEnum.NPS,
|
||||
TSurveyQuestionTypeEnum.Date,
|
||||
].includes(question.type)
|
||||
);
|
||||
];
|
||||
|
||||
if (selectedQuestion.inputType !== "number") {
|
||||
allowedQuestionTypes.push(TSurveyQuestionTypeEnum.Date);
|
||||
}
|
||||
|
||||
if (["equals", "doesNotEqual"].includes(condition.operator)) {
|
||||
allowedQuestionTypes.push(TSurveyQuestionTypeEnum.MultipleChoiceMulti);
|
||||
}
|
||||
|
||||
return allowedQuestionTypes.includes(question.type);
|
||||
});
|
||||
|
||||
const questionOptions = allowedQuestions.map((question) => {
|
||||
return {
|
||||
@@ -231,16 +239,20 @@ export const getMatchValueProps = (
|
||||
};
|
||||
});
|
||||
|
||||
const variableOptions = variables.map((variable) => {
|
||||
return {
|
||||
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
|
||||
label: variable.name,
|
||||
value: variable.id,
|
||||
meta: {
|
||||
type: "variable",
|
||||
},
|
||||
};
|
||||
});
|
||||
const variableOptions = variables
|
||||
.filter((variable) =>
|
||||
selectedQuestion.inputType !== "number" ? variable.type === "text" : variable.type === "number"
|
||||
)
|
||||
.map((variable) => {
|
||||
return {
|
||||
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
|
||||
label: variable.name,
|
||||
value: variable.id,
|
||||
meta: {
|
||||
type: "variable",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const hiddenFieldsOptions = hiddenFields.map((field) => {
|
||||
return {
|
||||
@@ -414,8 +426,8 @@ export const getMatchValueProps = (
|
||||
options: groupedOptions,
|
||||
};
|
||||
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.Date) {
|
||||
const openTextQuestions = questions.filter(
|
||||
(question) => question.type === TSurveyQuestionTypeEnum.OpenText
|
||||
const openTextQuestions = questions.filter((question) =>
|
||||
[TSurveyQuestionTypeEnum.OpenText, TSurveyQuestionTypeEnum.Date].includes(question.type)
|
||||
);
|
||||
|
||||
const questionOptions = openTextQuestions.map((question) => {
|
||||
|
||||
@@ -2,9 +2,11 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
TResponse,
|
||||
TResponseData,
|
||||
TResponseFilterCriteria,
|
||||
TResponseHiddenFieldsFilter,
|
||||
TResponseTtc,
|
||||
TResponseVariables,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
@@ -28,7 +30,7 @@ import {
|
||||
import { getLocalizedValue } from "../i18n/utils";
|
||||
import { processResponseData } from "../responses";
|
||||
import { getTodaysDateTimeFormatted } from "../time";
|
||||
import { evaluateAdvancedLogic } from "../utils/evaluateLogic";
|
||||
import { evaluateAdvancedLogic, performActions } from "../utils/evaluateLogic";
|
||||
import { sanitizeString } from "../utils/strings";
|
||||
|
||||
export const calculateTtcTotal = (ttc: TResponseTtc) => {
|
||||
@@ -618,6 +620,11 @@ export const getSurveySummaryDropOff = (
|
||||
let impressionsArr = new Array(survey.questions.length).fill(0) as number[];
|
||||
let dropOffPercentageArr = new Array(survey.questions.length).fill(0) as number[];
|
||||
|
||||
const surveyVariablesData = survey.variables?.reduce((acc, variable) => {
|
||||
acc[variable.id] = variable.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
responses.forEach((response) => {
|
||||
// Calculate total time-to-completion
|
||||
Object.keys(totalTtc).forEach((questionId) => {
|
||||
@@ -627,43 +634,40 @@ export const getSurveySummaryDropOff = (
|
||||
}
|
||||
});
|
||||
|
||||
let localSurvey = JSON.parse(JSON.stringify(survey)) as TSurvey;
|
||||
let localResponseData: TResponseData = { ...response.data };
|
||||
let localVariables: TResponseVariables = {
|
||||
...surveyVariablesData,
|
||||
};
|
||||
|
||||
let currQuesIdx = 0;
|
||||
|
||||
while (currQuesIdx < survey.questions.length) {
|
||||
const currQues = survey.questions[currQuesIdx];
|
||||
while (currQuesIdx < localSurvey.questions.length) {
|
||||
const currQues = localSurvey.questions[currQuesIdx];
|
||||
if (!currQues) break;
|
||||
|
||||
if (!currQues.required) {
|
||||
if (!response.data[currQues.id]) {
|
||||
impressionsArr[currQuesIdx]++;
|
||||
|
||||
if (currQuesIdx === survey.questions.length - 1 && !response.finished) {
|
||||
dropOffArr[currQuesIdx]++;
|
||||
break;
|
||||
}
|
||||
|
||||
const nextQuestionId = getNextQuestionId(survey, currQues, response.data);
|
||||
if (nextQuestionId) {
|
||||
currQuesIdx = survey.questions.findIndex((q) => q.id === nextQuestionId);
|
||||
} else {
|
||||
currQuesIdx++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(response.data[currQues.id] === undefined && !response.finished) ||
|
||||
(currQues.required && !response.data[currQues.id])
|
||||
) {
|
||||
// question is not answered and required
|
||||
if (response.data[currQues.id] === undefined && currQues.required) {
|
||||
dropOffArr[currQuesIdx]++;
|
||||
impressionsArr[currQuesIdx]++;
|
||||
break;
|
||||
}
|
||||
|
||||
localResponseData[currQues.id] = response.data[currQues.id];
|
||||
impressionsArr[currQuesIdx]++;
|
||||
|
||||
const nextQuestionId = getNextQuestionId(survey, currQues, response.data);
|
||||
const { nextQuestionId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextQuestionId(
|
||||
localSurvey,
|
||||
localResponseData,
|
||||
localVariables,
|
||||
currQuesIdx,
|
||||
currQues,
|
||||
response.language
|
||||
);
|
||||
|
||||
localSurvey = updatedSurvey;
|
||||
localVariables = updatedVariables;
|
||||
|
||||
if (nextQuestionId) {
|
||||
const nextQuesIdx = survey.questions.findIndex((q) => q.id === nextQuestionId);
|
||||
if (!response.data[nextQuestionId] && !response.finished) {
|
||||
@@ -718,23 +722,57 @@ export const getSurveySummaryDropOff = (
|
||||
return dropOff;
|
||||
};
|
||||
|
||||
const getNextQuestionId = (
|
||||
survey: TSurvey,
|
||||
question: TSurveyQuestion,
|
||||
responseData: Record<string, any>
|
||||
): string | undefined => {
|
||||
if (!question.logic) return undefined;
|
||||
const evaluateLogicAndGetNextQuestionId = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
localVariables: TResponseVariables,
|
||||
currentQuestionIndex: number,
|
||||
currQuesTemp: TSurveyQuestion,
|
||||
selectedLanguage: string | null
|
||||
): { nextQuestionId: string | undefined; updatedSurvey: TSurvey; updatedVariables: TResponseVariables } => {
|
||||
const questions = localSurvey.questions;
|
||||
|
||||
for (const logic of question.logic) {
|
||||
if (evaluateAdvancedLogic(survey, responseData, logic.conditions, "default")) {
|
||||
const jumpAction = logic.actions.find((action) => action.objective === "jumpToQuestion");
|
||||
if (jumpAction) {
|
||||
return jumpAction.target;
|
||||
let updatedSurvey = { ...localSurvey };
|
||||
let updatedVariables = { ...localVariables };
|
||||
|
||||
let firstJumpTarget: string | undefined;
|
||||
|
||||
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
|
||||
for (const logic of currQuesTemp.logic) {
|
||||
if (
|
||||
evaluateAdvancedLogic(
|
||||
localSurvey,
|
||||
data,
|
||||
localVariables,
|
||||
logic.conditions,
|
||||
selectedLanguage ?? "default"
|
||||
)
|
||||
) {
|
||||
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
|
||||
updatedSurvey,
|
||||
logic.actions,
|
||||
data,
|
||||
updatedVariables
|
||||
);
|
||||
|
||||
if (requiredQuestionIds.length > 0) {
|
||||
updatedSurvey.questions = updatedSurvey.questions.map((q) =>
|
||||
requiredQuestionIds.includes(q.id) ? { ...q, required: true } : q
|
||||
);
|
||||
}
|
||||
updatedVariables = { ...updatedVariables, ...calculations };
|
||||
|
||||
if (jumpTarget && !firstJumpTarget) {
|
||||
firstJumpTarget = jumpTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// Return the first jump target if found, otherwise go to the next question
|
||||
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || undefined;
|
||||
|
||||
return { nextQuestionId, updatedSurvey, updatedVariables };
|
||||
};
|
||||
|
||||
const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => {
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import {
|
||||
TAction,
|
||||
TActionCalculate,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "../i18n/utils";
|
||||
import { isConditionsGroup } from "../survey/logic/utils";
|
||||
|
||||
export const evaluateAdvancedLogic = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
conditions: TConditionGroup,
|
||||
selectedLanguage: string
|
||||
): boolean => {
|
||||
@@ -15,7 +26,7 @@ export const evaluateAdvancedLogic = (
|
||||
if (isConditionsGroup(condition)) {
|
||||
return evaluateConditionGroup(condition);
|
||||
} else {
|
||||
return evaluateSingleCondition(localSurvey, data, condition, selectedLanguage);
|
||||
return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,101 +39,227 @@ export const evaluateAdvancedLogic = (
|
||||
const evaluateSingleCondition = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
condition: TSingleCondition,
|
||||
selectedLanguage: string
|
||||
): boolean => {
|
||||
const leftValue = getLeftOperandValue(localSurvey, data, condition.leftOperand, selectedLanguage);
|
||||
const rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, condition.rightOperand, data)
|
||||
: undefined;
|
||||
try {
|
||||
const leftValue = getLeftOperandValue(
|
||||
localSurvey,
|
||||
data,
|
||||
variablesData,
|
||||
condition.leftOperand,
|
||||
selectedLanguage
|
||||
);
|
||||
const rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
|
||||
: undefined;
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
leftValue.includes(rightValue)) ||
|
||||
leftValue?.toString() === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
return leftValue !== rightValue;
|
||||
case "contains":
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
case "doesNotContain":
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
case "startsWith":
|
||||
return String(leftValue).startsWith(String(rightValue));
|
||||
case "doesNotStartWith":
|
||||
return !String(leftValue).startsWith(String(rightValue));
|
||||
case "endsWith":
|
||||
return String(leftValue).endsWith(String(rightValue));
|
||||
case "doesNotEndWith":
|
||||
return !String(leftValue).endsWith(String(rightValue));
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
return leftValue !== "" && leftValue !== null;
|
||||
} else if (Array.isArray(leftValue)) {
|
||||
return leftValue.length > 0;
|
||||
} else if (typeof leftValue === "number") {
|
||||
return leftValue !== null;
|
||||
}
|
||||
return false;
|
||||
case "isSkipped":
|
||||
return (
|
||||
(Array.isArray(leftValue) && leftValue.length === 0) ||
|
||||
leftValue === "" ||
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0)
|
||||
);
|
||||
case "isGreaterThan":
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
case "isLessThan":
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
case "isGreaterThanOrEqual":
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
case "isLessThanOrEqual":
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
case "equalsOneOf":
|
||||
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
|
||||
case "includesAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => leftValue.includes(v))
|
||||
);
|
||||
case "includesOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) && Array.isArray(rightValue) && rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
case "isClicked":
|
||||
return leftValue === "clicked";
|
||||
case "isAfter":
|
||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
||||
case "isBefore":
|
||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
||||
case "isBooked":
|
||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||
case "isPartiallySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
return Object.values(leftValue).includes("");
|
||||
} else return false;
|
||||
case "isCompletelySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
const values = Object.values(leftValue);
|
||||
return values.length > 0 && !values.includes("");
|
||||
} else return false;
|
||||
default:
|
||||
return false;
|
||||
let leftField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
if (condition.leftOperand?.type === "question") {
|
||||
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion;
|
||||
} else if (condition.leftOperand?.type === "variable") {
|
||||
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
|
||||
} else if (condition.leftOperand?.type === "hiddenField") {
|
||||
leftField = condition.leftOperand.value as string;
|
||||
} else {
|
||||
leftField = "";
|
||||
}
|
||||
|
||||
let rightField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
rightField = localSurvey.questions.find(
|
||||
(q) => q.id === condition.rightOperand?.value
|
||||
) as TSurveyQuestion;
|
||||
} else if (condition.rightOperand?.type === "variable") {
|
||||
rightField = localSurvey.variables.find(
|
||||
(v) => v.id === condition.rightOperand?.value
|
||||
) as TSurveyVariable;
|
||||
} else if (condition.rightOperand?.type === "hiddenField") {
|
||||
rightField = condition.rightOperand.value as string;
|
||||
} else {
|
||||
rightField = "";
|
||||
}
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
// when left value is of multi choice, picture selection question and right value is its option
|
||||
if (Array.isArray(leftValue) && leftValue.length > 0 && typeof rightValue === "string") {
|
||||
return leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
) {
|
||||
if (Array.isArray(condition.rightOperand.value)) {
|
||||
return condition.rightOperand.value.includes(leftValue);
|
||||
} else {
|
||||
return leftValue === condition.rightOperand.value;
|
||||
}
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
leftValue.includes(rightValue)) ||
|
||||
leftValue?.toString() === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
// when left value is of multi choice, picture selection question and right value is its option
|
||||
if (Array.isArray(leftValue) && leftValue.length > 0 && typeof rightValue === "string") {
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
) {
|
||||
if (Array.isArray(condition.rightOperand.value)) {
|
||||
return !condition.rightOperand.value.includes(leftValue);
|
||||
} else {
|
||||
return leftValue !== condition.rightOperand.value;
|
||||
}
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return leftValue?.toString() !== rightValue;
|
||||
case "contains":
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
case "doesNotContain":
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
case "startsWith":
|
||||
return String(leftValue).startsWith(String(rightValue));
|
||||
case "doesNotStartWith":
|
||||
return !String(leftValue).startsWith(String(rightValue));
|
||||
case "endsWith":
|
||||
return String(leftValue).endsWith(String(rightValue));
|
||||
case "doesNotEndWith":
|
||||
return !String(leftValue).endsWith(String(rightValue));
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
|
||||
leftValue
|
||||
) {
|
||||
return leftValue !== "skipped";
|
||||
}
|
||||
return leftValue !== "" && leftValue !== null;
|
||||
} else if (Array.isArray(leftValue)) {
|
||||
return leftValue.length > 0;
|
||||
} else if (typeof leftValue === "number") {
|
||||
return leftValue !== null;
|
||||
}
|
||||
return false;
|
||||
case "isSkipped":
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload
|
||||
) {
|
||||
return leftValue === "skipped";
|
||||
}
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) && leftValue.length === 0) ||
|
||||
leftValue === "" ||
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0)
|
||||
);
|
||||
case "isGreaterThan":
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
case "isLessThan":
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
case "isGreaterThanOrEqual":
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
case "isLessThanOrEqual":
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
case "equalsOneOf":
|
||||
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
|
||||
case "includesAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => leftValue.includes(v))
|
||||
);
|
||||
case "includesOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
case "isClicked":
|
||||
return leftValue === "clicked";
|
||||
case "isAfter":
|
||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
||||
case "isBefore":
|
||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
||||
case "isBooked":
|
||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||
case "isPartiallySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
return Object.values(leftValue).includes("");
|
||||
} else return false;
|
||||
case "isCompletelySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
const values = Object.values(leftValue);
|
||||
return values.length > 0 && !values.includes("");
|
||||
} else return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getLeftOperandValue = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
leftOperand: TSingleCondition["leftOperand"],
|
||||
selectedLanguage: string
|
||||
) => {
|
||||
@@ -163,7 +300,7 @@ const getLeftOperandValue = (
|
||||
|
||||
if (!variable) return undefined;
|
||||
|
||||
const variableValue = data[leftOperand.value];
|
||||
const variableValue = variablesData[leftOperand.value];
|
||||
|
||||
if (variable.type === "number") return Number(variableValue) || 0;
|
||||
return variableValue || "";
|
||||
@@ -176,8 +313,9 @@ const getLeftOperandValue = (
|
||||
|
||||
const getRightOperandValue = (
|
||||
localSurvey: TSurvey,
|
||||
rightOperand: TSingleCondition["rightOperand"],
|
||||
data: TResponseData
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
rightOperand: TSingleCondition["rightOperand"]
|
||||
) => {
|
||||
if (!rightOperand) return undefined;
|
||||
|
||||
@@ -190,7 +328,7 @@ const getRightOperandValue = (
|
||||
|
||||
if (!variable) return undefined;
|
||||
|
||||
const variableValue = data[rightOperand.value];
|
||||
const variableValue = variablesData[rightOperand.value];
|
||||
|
||||
if (variable.type === "number") return Number(variableValue) || 0;
|
||||
return variableValue || "";
|
||||
@@ -202,3 +340,106 @@ const getRightOperandValue = (
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const performActions = (
|
||||
survey: TSurvey,
|
||||
actions: TAction[],
|
||||
data: TResponseData,
|
||||
calculationResults: TResponseVariables
|
||||
): {
|
||||
jumpTarget: string | undefined;
|
||||
requiredQuestionIds: string[];
|
||||
calculations: TResponseVariables;
|
||||
} => {
|
||||
let jumpTarget: string | undefined;
|
||||
const requiredQuestionIds: string[] = [];
|
||||
const calculations: TResponseVariables = { ...calculationResults };
|
||||
|
||||
actions.forEach((action) => {
|
||||
switch (action.objective) {
|
||||
case "calculate":
|
||||
const result = performCalculation(survey, action, data, calculations);
|
||||
if (result !== undefined) calculations[action.variableId] = result;
|
||||
break;
|
||||
case "requireAnswer":
|
||||
requiredQuestionIds.push(action.target);
|
||||
break;
|
||||
case "jumpToQuestion":
|
||||
if (!jumpTarget) {
|
||||
jumpTarget = action.target;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return { jumpTarget, requiredQuestionIds, calculations };
|
||||
};
|
||||
|
||||
const performCalculation = (
|
||||
survey: TSurvey,
|
||||
action: TActionCalculate,
|
||||
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 = 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
|
||||
switch (action.value.type) {
|
||||
case "static":
|
||||
operandValue = action.value.value;
|
||||
break;
|
||||
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") {
|
||||
if (variable.type === "number" && !isNaN(Number(val))) {
|
||||
operandValue = Number(val);
|
||||
}
|
||||
operandValue = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (operandValue === undefined || operandValue === null) return undefined;
|
||||
|
||||
let result: number | string;
|
||||
|
||||
switch (action.operator) {
|
||||
case "add":
|
||||
result = Number(currentValue) + Number(operandValue);
|
||||
break;
|
||||
case "subtract":
|
||||
result = Number(currentValue) - Number(operandValue);
|
||||
break;
|
||||
case "multiply":
|
||||
result = Number(currentValue) * Number(operandValue);
|
||||
break;
|
||||
case "divide":
|
||||
if (Number(operandValue) === 0) return undefined;
|
||||
result = Number(currentValue) / Number(operandValue);
|
||||
break;
|
||||
case "assign":
|
||||
result = operandValue;
|
||||
break;
|
||||
case "concat":
|
||||
result = String(currentValue) + String(operandValue);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -43,123 +43,216 @@ const evaluateSingleCondition = (
|
||||
condition: TSingleCondition,
|
||||
selectedLanguage: string
|
||||
): boolean => {
|
||||
const leftValue = getLeftOperandValue(
|
||||
localSurvey,
|
||||
data,
|
||||
variablesData,
|
||||
condition.leftOperand,
|
||||
selectedLanguage
|
||||
);
|
||||
const rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
|
||||
: undefined;
|
||||
try {
|
||||
const leftValue = getLeftOperandValue(
|
||||
localSurvey,
|
||||
data,
|
||||
variablesData,
|
||||
condition.leftOperand,
|
||||
selectedLanguage
|
||||
);
|
||||
const rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
|
||||
: undefined;
|
||||
|
||||
let leftField: TSurveyQuestion | TSurveyVariable | string;
|
||||
let leftField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
switch (condition.leftOperand.type) {
|
||||
case "question":
|
||||
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand.value) as TSurveyQuestion;
|
||||
break;
|
||||
case "variable":
|
||||
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand.value) as TSurveyVariable;
|
||||
break;
|
||||
case "hiddenField":
|
||||
if (condition.leftOperand?.type === "question") {
|
||||
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion;
|
||||
} else if (condition.leftOperand?.type === "variable") {
|
||||
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
|
||||
} else if (condition.leftOperand?.type === "hiddenField") {
|
||||
leftField = condition.leftOperand.value as string;
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
leftField = "";
|
||||
}
|
||||
}
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
leftValue.includes(rightValue)) ||
|
||||
leftValue?.toString() === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
return leftValue !== rightValue;
|
||||
case "contains":
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
case "doesNotContain":
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
case "startsWith":
|
||||
return String(leftValue).startsWith(String(rightValue));
|
||||
case "doesNotStartWith":
|
||||
return !String(leftValue).startsWith(String(rightValue));
|
||||
case "endsWith":
|
||||
return String(leftValue).endsWith(String(rightValue));
|
||||
case "doesNotEndWith":
|
||||
return !String(leftValue).endsWith(String(rightValue));
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
let rightField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
rightField = localSurvey.questions.find(
|
||||
(q) => q.id === condition.rightOperand?.value
|
||||
) as TSurveyQuestion;
|
||||
} else if (condition.rightOperand?.type === "variable") {
|
||||
rightField = localSurvey.variables.find(
|
||||
(v) => v.id === condition.rightOperand?.value
|
||||
) as TSurveyVariable;
|
||||
} else if (condition.rightOperand?.type === "hiddenField") {
|
||||
rightField = condition.rightOperand.value as string;
|
||||
} else {
|
||||
rightField = "";
|
||||
}
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
// when left value is of multi choice, picture selection question and right value is its option
|
||||
if (Array.isArray(leftValue) && leftValue.length > 0 && typeof rightValue === "string") {
|
||||
return leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
) {
|
||||
if (Array.isArray(condition.rightOperand.value)) {
|
||||
return condition.rightOperand.value.includes(leftValue);
|
||||
} else {
|
||||
return leftValue === condition.rightOperand.value;
|
||||
}
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
leftValue.includes(rightValue)) ||
|
||||
leftValue?.toString() === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
// when left value is of multi choice, picture selection question and right value is its option
|
||||
if (Array.isArray(leftValue) && leftValue.length > 0 && typeof rightValue === "string") {
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
|
||||
) {
|
||||
if (Array.isArray(condition.rightOperand.value)) {
|
||||
return !condition.rightOperand.value.includes(leftValue);
|
||||
} else {
|
||||
return leftValue !== condition.rightOperand.value;
|
||||
}
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return leftValue?.toString() !== rightValue;
|
||||
case "contains":
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
case "doesNotContain":
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
case "startsWith":
|
||||
return String(leftValue).startsWith(String(rightValue));
|
||||
case "doesNotStartWith":
|
||||
return !String(leftValue).startsWith(String(rightValue));
|
||||
case "endsWith":
|
||||
return String(leftValue).endsWith(String(rightValue));
|
||||
case "doesNotEndWith":
|
||||
return !String(leftValue).endsWith(String(rightValue));
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
|
||||
leftValue
|
||||
) {
|
||||
return leftValue !== "skipped";
|
||||
}
|
||||
return leftValue !== "" && leftValue !== null;
|
||||
} else if (Array.isArray(leftValue)) {
|
||||
return leftValue.length > 0;
|
||||
} else if (typeof leftValue === "number") {
|
||||
return leftValue !== null;
|
||||
}
|
||||
return false;
|
||||
case "isSkipped":
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload
|
||||
) {
|
||||
return leftValue !== "skipped" && leftValue !== "" && leftValue !== null;
|
||||
return leftValue === "skipped";
|
||||
}
|
||||
return leftValue !== "" && leftValue !== null;
|
||||
} else if (Array.isArray(leftValue)) {
|
||||
return leftValue.length > 0;
|
||||
} else if (typeof leftValue === "number") {
|
||||
return leftValue !== null;
|
||||
}
|
||||
return false;
|
||||
case "isSkipped":
|
||||
return (
|
||||
(Array.isArray(leftValue) && leftValue.length === 0) ||
|
||||
leftValue === "" ||
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0) ||
|
||||
(condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
|
||||
leftValue === "skipped")
|
||||
);
|
||||
case "isGreaterThan":
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
case "isLessThan":
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
case "isGreaterThanOrEqual":
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
case "isLessThanOrEqual":
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
case "equalsOneOf":
|
||||
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
|
||||
case "includesAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => leftValue.includes(v))
|
||||
);
|
||||
case "includesOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) && Array.isArray(rightValue) && rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
case "isClicked":
|
||||
return leftValue === "clicked";
|
||||
case "isAfter":
|
||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
||||
case "isBefore":
|
||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
||||
case "isBooked":
|
||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||
case "isPartiallySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
return Object.values(leftValue).includes("");
|
||||
} else return false;
|
||||
case "isCompletelySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
const values = Object.values(leftValue);
|
||||
return values.length > 0 && !values.includes("");
|
||||
} else return false;
|
||||
default:
|
||||
return false;
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) && leftValue.length === 0) ||
|
||||
leftValue === "" ||
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0)
|
||||
);
|
||||
case "isGreaterThan":
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
case "isLessThan":
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
case "isGreaterThanOrEqual":
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
case "isLessThanOrEqual":
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
case "equalsOneOf":
|
||||
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
|
||||
case "includesAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => leftValue.includes(v))
|
||||
);
|
||||
case "includesOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
case "isClicked":
|
||||
return leftValue === "clicked";
|
||||
case "isAfter":
|
||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
||||
case "isBefore":
|
||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
||||
case "isBooked":
|
||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||
case "isPartiallySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
return Object.values(leftValue).includes("");
|
||||
} else return false;
|
||||
case "isCompletelySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
const values = Object.values(leftValue);
|
||||
return values.length > 0 && !values.includes("");
|
||||
} else return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user