diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util.tsx index df696e1992..950cc444f3 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util.tsx @@ -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) => { diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index 79cf9c9eb0..8e20be4feb 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -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 | 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) => { diff --git a/packages/lib/utils/evaluateLogic.ts b/packages/lib/utils/evaluateLogic.ts index f2b553ee5c..22b8856bdb 100644 --- a/packages/lib/utils/evaluateLogic.ts +++ b/packages/lib/utils/evaluateLogic.ts @@ -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 +): 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; +}; diff --git a/packages/surveys/src/lib/logicEvaluator.ts b/packages/surveys/src/lib/logicEvaluator.ts index f20c86b543..3b205b0cf3 100644 --- a/packages/surveys/src/lib/logicEvaluator.ts +++ b/packages/surveys/src/lib/logicEvaluator.ts @@ -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; } };