From 2b0df8280d703a99eff8d5eb4d9c8e779b148ebd Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 24 Sep 2024 09:51:26 +0530 Subject: [PATCH] fix: build error --- .../xm-templates/lib/xm-templates.ts | 594 ++++++++++++------ .../edit/components/ConditionalLogic.tsx | 2 +- .../edit/components/LogicEditorActions.tsx | 2 +- .../edit/components/LogicEditorConditions.tsx | 2 +- .../edit/components/QuestionsView.tsx | 2 +- .../surveys/[surveyId]/edit/lib/utils.tsx | 2 +- packages/lib/response/utils.ts | 4 +- packages/lib/survey/logic/utils.ts | 205 ------ packages/lib/survey/tests/survey.test.ts | 2 +- .../evaluateLogic.ts => surveyLogic/utils.ts} | 201 +++++- .../surveys/src/components/general/Survey.tsx | 2 +- packages/surveys/src/lib/logicEvaluator.ts | 459 -------------- 12 files changed, 596 insertions(+), 881 deletions(-) delete mode 100644 packages/lib/survey/logic/utils.ts rename packages/lib/{utils/evaluateLogic.ts => surveyLogic/utils.ts} (75%) delete mode 100644 packages/surveys/src/lib/logicEvaluator.ts diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts index 5d326dc8cf..aeda96aada 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates.ts @@ -12,215 +12,395 @@ export const XMSurveyDefault: TXMTemplate = { }, }; -const NPSSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "NPS Survey", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.NPS, - headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" }, - required: true, - lowerLabel: { default: "Not at all likely" }, - upperLabel: { default: "Extremely likely" }, - isColorCodingEnabled: true, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, - required: false, - inputType: "text", - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Any other comments, feedback, or concerns?" }, - required: false, - inputType: "text", - }, - ], -}; - -const StarRatingSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "{{productName}}'s Rating Survey", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - logic: [{ value: 3, condition: "lessEqual", destination: "tk9wpw2gxgb8fa6pbpp3qq5l" }], - range: 5, - scale: "number", - headline: { default: "How do you like {{productName}}?" }, - required: true, - lowerLabel: { default: "Extremely dissatisfied" }, - upperLabel: { default: "Extremely satisfied" }, - isColorCodingEnabled: false, - }, - { - id: createId(), - html: { default: '

This helps us a lot.

' }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [{ condition: "clicked", destination: XMSurveyDefault.endings[0].id }], - headline: { default: "Happy to hear 🙏 Please write a review for us!" }, - required: true, - buttonUrl: "https://formbricks.com/github", - buttonLabel: { default: "Write review" }, - buttonExternal: true, - }, - { - id: "tk9wpw2gxgb8fa6pbpp3qq5l", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, - required: true, - subheader: { default: "Help us improve your experience." }, - buttonLabel: { default: "Send" }, - placeholder: { default: "Type your answer here..." }, - inputType: "text", - }, - ], -}; - -const CSATSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "{{productName}} CSAT", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - logic: [{ value: 3, condition: "lessEqual", destination: "vyo4mkw4ln95ts4ya7qp2tth" }], - range: 5, - scale: "smiley", - headline: { default: "How satisfied are you with your {{productName}} experience?" }, - required: true, - lowerLabel: { default: "Extremely dissatisfied" }, - upperLabel: { default: "Extremely satisfied" }, - isColorCodingEnabled: false, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - logic: [{ condition: "submitted", destination: XMSurveyDefault.endings[0].id }], - headline: { default: "Lovely! Is there anything we can do to improve your experience?" }, - required: false, - placeholder: { default: "Type your answer here..." }, - inputType: "text", - }, - { - id: "vyo4mkw4ln95ts4ya7qp2tth", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" }, - required: false, - placeholder: { default: "Type your answer here..." }, - inputType: "text", - }, - ], -}; - -const CESSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "CES Survey", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - range: 5, - scale: "number", - headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" }, - required: true, - lowerLabel: { default: "Disagree strongly" }, - upperLabel: { default: "Agree strongly" }, - isColorCodingEnabled: false, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" }, - required: true, - placeholder: { default: "Type your answer here..." }, - inputType: "text", - }, - ], -}; - -const SmileysRatingSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "Smileys Survey", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.Rating, - logic: [{ value: 3, condition: "lessEqual", destination: "tk9wpw2gxgb8fa6pbpp3qq5l" }], - range: 5, - scale: "smiley", - headline: { default: "How do you like {{productName}}?" }, - required: true, - lowerLabel: { default: "Not good" }, - upperLabel: { default: "Very satisfied" }, - isColorCodingEnabled: false, - }, - { - id: createId(), - html: { default: '

This helps us a lot.

' }, - type: TSurveyQuestionTypeEnum.CTA, - logic: [{ condition: "clicked", destination: XMSurveyDefault.endings[0].id }], - headline: { default: "Happy to hear 🙏 Please write a review for us!" }, - required: true, - buttonUrl: "https://formbricks.com/github", - buttonLabel: { default: "Write review" }, - buttonExternal: true, - }, - { - id: "tk9wpw2gxgb8fa6pbpp3qq5l", - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, - required: true, - subheader: { default: "Help us improve your experience." }, - buttonLabel: { default: "Send" }, - placeholder: { default: "Type your answer here..." }, - inputType: "text", - }, - ], -}; - -const eNPSSurvey: TXMTemplate = { - ...XMSurveyDefault, - name: "eNPS Survey", - questions: [ - { - id: createId(), - type: TSurveyQuestionTypeEnum.NPS, - headline: { - default: "How likely are you to recommend working at this company to a friend or colleague?", +const NPSSurvey = (): TXMTemplate => { + return { + ...XMSurveyDefault, + name: "NPS Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.NPS, + headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" }, + required: true, + lowerLabel: { default: "Not at all likely" }, + upperLabel: { default: "Extremely likely" }, + isColorCodingEnabled: true, }, - required: false, - lowerLabel: { default: "Not at all likely" }, - upperLabel: { default: "Extremely likely" }, - isColorCodingEnabled: true, - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, - required: false, - inputType: "text", - }, - { - id: createId(), - type: TSurveyQuestionTypeEnum.OpenText, - headline: { default: "Any other comments, feedback, or concerns?" }, - required: false, - inputType: "text", - }, - ], + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, + required: false, + inputType: "text", + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Any other comments, feedback, or concerns?" }, + required: false, + inputType: "text", + }, + ], + }; +}; + +const StarRatingSurvey = (): TXMTemplate => { + const reusableQuestionIds = [createId(), createId(), createId()]; + + return { + ...XMSurveyDefault, + name: "{{productName}}'s Rating Survey", + questions: [ + { + id: reusableQuestionIds[0], + type: TSurveyQuestionTypeEnum.Rating, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[0], + type: "question", + }, + operator: "isLessThanOrEqual", + rightOperand: { + type: "static", + value: 3, + }, + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: reusableQuestionIds[2], + }, + ], + }, + ], + range: 5, + scale: "number", + headline: { default: "How do you like {{productName}}?" }, + required: true, + lowerLabel: { default: "Extremely dissatisfied" }, + upperLabel: { default: "Extremely satisfied" }, + isColorCodingEnabled: false, + }, + { + id: reusableQuestionIds[1], + html: { default: '

This helps us a lot.

' }, + type: TSurveyQuestionTypeEnum.CTA, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[1], + type: "question", + }, + operator: "isClicked", + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: XMSurveyDefault.endings[0].id, + }, + ], + }, + ], + headline: { default: "Happy to hear 🙏 Please write a review for us!" }, + required: true, + buttonUrl: "https://formbricks.com/github", + buttonLabel: { default: "Write review" }, + buttonExternal: true, + }, + { + id: reusableQuestionIds[2], + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, + required: true, + subheader: { default: "Help us improve your experience." }, + buttonLabel: { default: "Send" }, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], + }; +}; + +const CSATSurvey = (): TXMTemplate => { + const reusableQuestionIds = [createId(), createId(), createId()]; + + return { + ...XMSurveyDefault, + name: "{{productName}} CSAT", + questions: [ + { + id: reusableQuestionIds[0], + type: TSurveyQuestionTypeEnum.Rating, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[0], + type: "question", + }, + operator: "isLessThanOrEqual", + rightOperand: { + type: "static", + value: 3, + }, + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: reusableQuestionIds[2], + }, + ], + }, + ], + range: 5, + scale: "smiley", + headline: { default: "How satisfied are you with your {{productName}} experience?" }, + required: true, + lowerLabel: { default: "Extremely dissatisfied" }, + upperLabel: { default: "Extremely satisfied" }, + isColorCodingEnabled: false, + }, + { + id: reusableQuestionIds[1], + type: TSurveyQuestionTypeEnum.OpenText, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[1], + type: "question", + }, + operator: "isSubmitted", + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: XMSurveyDefault.endings[0].id, + }, + ], + }, + ], + headline: { default: "Lovely! Is there anything we can do to improve your experience?" }, + required: false, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + { + id: reusableQuestionIds[2], + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" }, + required: false, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], + }; +}; + +const CESSurvey = (): TXMTemplate => { + return { + ...XMSurveyDefault, + name: "CES Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.Rating, + range: 5, + scale: "number", + headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" }, + required: true, + lowerLabel: { default: "Disagree strongly" }, + upperLabel: { default: "Agree strongly" }, + isColorCodingEnabled: false, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" }, + required: true, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], + }; +}; + +const SmileysRatingSurvey = (): TXMTemplate => { + const reusableQuestionIds = [createId(), createId(), createId()]; + + return { + ...XMSurveyDefault, + name: "Smileys Survey", + questions: [ + { + id: reusableQuestionIds[0], + type: TSurveyQuestionTypeEnum.Rating, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[0], + type: "question", + }, + operator: "isLessThanOrEqual", + rightOperand: { + type: "static", + value: 3, + }, + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: reusableQuestionIds[2], + }, + ], + }, + ], + range: 5, + scale: "smiley", + headline: { default: "How do you like {{productName}}?" }, + required: true, + lowerLabel: { default: "Not good" }, + upperLabel: { default: "Very satisfied" }, + isColorCodingEnabled: false, + }, + { + id: reusableQuestionIds[1], + html: { default: '

This helps us a lot.

' }, + type: TSurveyQuestionTypeEnum.CTA, + logic: [ + { + id: createId(), + conditions: { + id: createId(), + connector: "and", + conditions: [ + { + id: createId(), + leftOperand: { + value: reusableQuestionIds[1], + type: "question", + }, + operator: "isClicked", + }, + ], + }, + actions: [ + { + id: createId(), + objective: "jumpToQuestion", + target: XMSurveyDefault.endings[0].id, + }, + ], + }, + ], + headline: { default: "Happy to hear 🙏 Please write a review for us!" }, + required: true, + buttonUrl: "https://formbricks.com/github", + buttonLabel: { default: "Write review" }, + buttonExternal: true, + }, + { + id: reusableQuestionIds[2], + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, + required: true, + subheader: { default: "Help us improve your experience." }, + buttonLabel: { default: "Send" }, + placeholder: { default: "Type your answer here..." }, + inputType: "text", + }, + ], + }; +}; + +const eNPSSurvey = (): TXMTemplate => { + return { + ...XMSurveyDefault, + name: "eNPS Survey", + questions: [ + { + id: createId(), + type: TSurveyQuestionTypeEnum.NPS, + headline: { + default: "How likely are you to recommend working at this company to a friend or colleague?", + }, + required: false, + lowerLabel: { default: "Not at all likely" }, + upperLabel: { default: "Extremely likely" }, + isColorCodingEnabled: true, + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "To help us improve, can you describe the reason(s) for your rating?" }, + required: false, + inputType: "text", + }, + { + id: createId(), + type: TSurveyQuestionTypeEnum.OpenText, + headline: { default: "Any other comments, feedback, or concerns?" }, + required: false, + inputType: "text", + }, + ], + }; }; export const XMTemplates: TXMTemplate[] = [ - NPSSurvey, - StarRatingSurvey, - CSATSurvey, - CESSurvey, - SmileysRatingSurvey, - eNPSSurvey, + NPSSurvey(), + StarRatingSurvey(), + CSATSurvey(), + CESSurvey(), + SmileysRatingSurvey(), + eNPSSurvey(), ]; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx index f2d11201a3..e7264fe6d4 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic.tsx @@ -11,7 +11,7 @@ import { TrashIcon, } from "lucide-react"; import { useMemo } from "react"; -import { duplicateLogicItem } from "@formbricks/lib/survey/logic/utils"; +import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils"; import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx index c70e72c94e..621cfd0ae1 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx @@ -7,7 +7,7 @@ import { } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils"; import { createId } from "@paralleldrive/cuid2"; import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, TrashIcon } from "lucide-react"; -import { getUpdatedActionBody } from "@formbricks/lib/survey/logic/utils"; +import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils"; import { TActionNumberVariableCalculateOperator, TActionObjective, diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx index 8b9a8d2e97..25dde6ba04 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx @@ -15,7 +15,7 @@ import { removeCondition, toggleGroupConnector, updateCondition, -} from "@formbricks/lib/survey/logic/utils"; +} from "@formbricks/lib/surveyLogic/utils"; import { TConditionGroup, TDyanmicLogicField, diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx index ce4096cec0..c95f623fd9 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx @@ -18,7 +18,7 @@ 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 { isConditionGroup } from "@formbricks/lib/survey/logic/utils"; +import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils"; import { getDefaultEndingCard } from "@formbricks/lib/templates"; import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx index fb1b48f75a..a6db2995a8 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx @@ -1,7 +1,7 @@ import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react"; import { HTMLInputTypeAttribute } from "react"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { isConditionGroup } from "@formbricks/lib/survey/logic/utils"; +import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils"; import { questionTypes } from "@formbricks/lib/utils/questions"; import { TConditionGroup, diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index c90fae92d4..0fddb5cd6d 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -1,6 +1,5 @@ import "server-only"; import { Prisma } from "@prisma/client"; -import { structuredClone } from "pollyfills/structuredClone"; import { TResponse, TResponseData, @@ -29,9 +28,10 @@ import { TSurveySummary, } from "@formbricks/types/surveys/types"; import { getLocalizedValue } from "../i18n/utils"; +import { structuredClone } from "../pollyfills/structuredClone"; import { processResponseData } from "../responses"; +import { evaluateLogic, performActions } from "../surveyLogic/utils"; import { getTodaysDateTimeFormatted } from "../time"; -import { evaluateLogic, performActions } from "../utils/evaluateLogic"; import { sanitizeString } from "../utils/strings"; export const calculateTtcTotal = (ttc: TResponseTtc) => { diff --git a/packages/lib/survey/logic/utils.ts b/packages/lib/survey/logic/utils.ts deleted file mode 100644 index 0dc46a416c..0000000000 --- a/packages/lib/survey/logic/utils.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { createId } from "@paralleldrive/cuid2"; -import { - TActionObjective, - TConditionGroup, - TSingleCondition, - TSurveyLogic, - TSurveyLogicAction, -} from "@formbricks/types/surveys/types"; - -type TCondition = TSingleCondition | TConditionGroup; - -export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => { - return (condition as TConditionGroup).connector !== undefined; -}; - -export const duplicateLogicItem = (logicItem: TSurveyLogic): TSurveyLogic => { - const duplicateConditionGroup = (group: TConditionGroup): TConditionGroup => { - return { - ...group, - id: createId(), - conditions: group.conditions.map((condition) => { - if (isConditionGroup(condition)) { - return duplicateConditionGroup(condition); - } else { - return duplicateCondition(condition); - } - }), - }; - }; - - const duplicateCondition = (condition: TSingleCondition): TSingleCondition => { - return { - ...condition, - id: createId(), - }; - }; - - const duplicateAction = (action: TSurveyLogicAction): TSurveyLogicAction => { - return { - ...action, - id: createId(), - }; - }; - - return { - ...logicItem, - id: createId(), - conditions: duplicateConditionGroup(logicItem.conditions), - actions: logicItem.actions.map(duplicateAction), - }; -}; - -export const addConditionBelow = ( - group: TConditionGroup, - resourceId: string, - condition: TSingleCondition -) => { - for (let i = 0; i < group.conditions.length; i++) { - const item = group.conditions[i]; - - if (isConditionGroup(item)) { - if (item.id === resourceId) { - group.conditions.splice(i + 1, 0, condition); - break; - } else { - addConditionBelow(item, resourceId, condition); - } - } else { - if (item.id === resourceId) { - group.conditions.splice(i + 1, 0, condition); - break; - } - } - } -}; - -export const toggleGroupConnector = (group: TConditionGroup, resourceId: string) => { - if (group.id === resourceId) { - group.connector = group.connector === "and" ? "or" : "and"; - return; - } - - for (const condition of group.conditions) { - if (condition.connector) { - toggleGroupConnector(condition, resourceId); - } - } -}; - -export const removeCondition = (group: TConditionGroup, resourceId: string) => { - for (let i = 0; i < group.conditions.length; i++) { - const item = group.conditions[i]; - - if (item.id === resourceId) { - group.conditions.splice(i, 1); - return; - } - - if (isConditionGroup(item)) { - removeCondition(item, resourceId); - } - } - - deleteEmptyGroups(group); -}; - -export const duplicateCondition = (group: TConditionGroup, resourceId: string) => { - for (let i = 0; i < group.conditions.length; i++) { - const item = group.conditions[i]; - - if (item.id === resourceId) { - const newItem: TCondition = { - ...item, - id: createId(), - }; - group.conditions.splice(i + 1, 0, newItem); - return; - } - - if (item.connector) { - duplicateCondition(item, resourceId); - } - } -}; - -export const deleteEmptyGroups = (group: TConditionGroup) => { - for (let i = 0; i < group.conditions.length; i++) { - const resource = group.conditions[i]; - - if (isConditionGroup(resource) && resource.conditions.length === 0) { - group.conditions.splice(i, 1); - } else if (isConditionGroup(resource)) { - deleteEmptyGroups(resource); - } - } -}; - -export const createGroupFromResource = (group: TConditionGroup, resourceId: string) => { - for (let i = 0; i < group.conditions.length; i++) { - const item = group.conditions[i]; - - if (item.id === resourceId) { - const newGroup: TConditionGroup = { - id: createId(), - connector: "and", - conditions: [item], - }; - group.conditions[i] = newGroup; - group.connector = group.connector ?? "and"; - return; - } - - if (isConditionGroup(item)) { - createGroupFromResource(item, resourceId); - } - } -}; - -export const updateCondition = ( - group: TConditionGroup, - resourceId: string, - condition: Partial -) => { - for (let i = 0; i < group.conditions.length; i++) { - const item = group.conditions[i]; - - if (item.id === resourceId && !("connector" in item)) { - group.conditions[i] = { ...item, ...condition } as TSingleCondition; - return; - } - - if (isConditionGroup(item)) { - updateCondition(item, resourceId, condition); - } - } -}; - -export const getUpdatedActionBody = ( - action: TSurveyLogicAction, - objective: TActionObjective -): TSurveyLogicAction => { - if (objective === action.objective) return action; - switch (objective) { - case "calculate": - return { - id: action.id, - objective: "calculate", - variableId: "", - operator: "assign", - value: { type: "static", value: "" }, - }; - case "requireAnswer": - return { - id: action.id, - objective: "requireAnswer", - target: "", - }; - case "jumpToQuestion": - return { - id: action.id, - objective: "jumpToQuestion", - target: "", - }; - } -}; diff --git a/packages/lib/survey/tests/survey.test.ts b/packages/lib/survey/tests/survey.test.ts index 9bb4d2a6af..5827d88156 100644 --- a/packages/lib/survey/tests/survey.test.ts +++ b/packages/lib/survey/tests/survey.test.ts @@ -1,7 +1,7 @@ import { prisma } from "../../__mocks__/database"; import { mockResponseNote, mockResponseWithMockPerson } from "../../response/tests/__mocks__/data.mock"; import { Prisma } from "@prisma/client"; -import { evaluateLogic } from "utils/evaluateLogic"; +import { evaluateLogic } from "surveyLogic/utils"; import { beforeEach, describe, expect, it } from "vitest"; import { testInputValidation } from "vitestSetup"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/packages/lib/utils/evaluateLogic.ts b/packages/lib/surveyLogic/utils.ts similarity index 75% rename from packages/lib/utils/evaluateLogic.ts rename to packages/lib/surveyLogic/utils.ts index f44a9b3e86..147827f5d6 100644 --- a/packages/lib/utils/evaluateLogic.ts +++ b/packages/lib/surveyLogic/utils.ts @@ -1,16 +1,215 @@ +import { createId } from "@paralleldrive/cuid2"; import { TResponseData, TResponseVariables } from "@formbricks/types/responses"; import { TActionCalculate, + TActionObjective, TConditionGroup, TSingleCondition, TSurvey, + TSurveyLogic, TSurveyLogicAction, TSurveyQuestion, TSurveyQuestionTypeEnum, TSurveyVariable, } from "@formbricks/types/surveys/types"; import { getLocalizedValue } from "../i18n/utils"; -import { isConditionGroup } from "../survey/logic/utils"; + +type TCondition = TSingleCondition | TConditionGroup; + +export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => { + return (condition as TConditionGroup).connector !== undefined; +}; + +export const duplicateLogicItem = (logicItem: TSurveyLogic): TSurveyLogic => { + const duplicateConditionGroup = (group: TConditionGroup): TConditionGroup => { + return { + ...group, + id: createId(), + conditions: group.conditions.map((condition) => { + if (isConditionGroup(condition)) { + return duplicateConditionGroup(condition); + } else { + return duplicateCondition(condition); + } + }), + }; + }; + + const duplicateCondition = (condition: TSingleCondition): TSingleCondition => { + return { + ...condition, + id: createId(), + }; + }; + + const duplicateAction = (action: TSurveyLogicAction): TSurveyLogicAction => { + return { + ...action, + id: createId(), + }; + }; + + return { + ...logicItem, + id: createId(), + conditions: duplicateConditionGroup(logicItem.conditions), + actions: logicItem.actions.map(duplicateAction), + }; +}; + +export const addConditionBelow = ( + group: TConditionGroup, + resourceId: string, + condition: TSingleCondition +) => { + for (let i = 0; i < group.conditions.length; i++) { + const item = group.conditions[i]; + + if (isConditionGroup(item)) { + if (item.id === resourceId) { + group.conditions.splice(i + 1, 0, condition); + break; + } else { + addConditionBelow(item, resourceId, condition); + } + } else { + if (item.id === resourceId) { + group.conditions.splice(i + 1, 0, condition); + break; + } + } + } +}; + +export const toggleGroupConnector = (group: TConditionGroup, resourceId: string) => { + if (group.id === resourceId) { + group.connector = group.connector === "and" ? "or" : "and"; + return; + } + + for (const condition of group.conditions) { + if (condition.connector) { + toggleGroupConnector(condition, resourceId); + } + } +}; + +export const removeCondition = (group: TConditionGroup, resourceId: string) => { + for (let i = 0; i < group.conditions.length; i++) { + const item = group.conditions[i]; + + if (item.id === resourceId) { + group.conditions.splice(i, 1); + return; + } + + if (isConditionGroup(item)) { + removeCondition(item, resourceId); + } + } + + deleteEmptyGroups(group); +}; + +export const duplicateCondition = (group: TConditionGroup, resourceId: string) => { + for (let i = 0; i < group.conditions.length; i++) { + const item = group.conditions[i]; + + if (item.id === resourceId) { + const newItem: TCondition = { + ...item, + id: createId(), + }; + group.conditions.splice(i + 1, 0, newItem); + return; + } + + if (item.connector) { + duplicateCondition(item, resourceId); + } + } +}; + +export const deleteEmptyGroups = (group: TConditionGroup) => { + for (let i = 0; i < group.conditions.length; i++) { + const resource = group.conditions[i]; + + if (isConditionGroup(resource) && resource.conditions.length === 0) { + group.conditions.splice(i, 1); + } else if (isConditionGroup(resource)) { + deleteEmptyGroups(resource); + } + } +}; + +export const createGroupFromResource = (group: TConditionGroup, resourceId: string) => { + for (let i = 0; i < group.conditions.length; i++) { + const item = group.conditions[i]; + + if (item.id === resourceId) { + const newGroup: TConditionGroup = { + id: createId(), + connector: "and", + conditions: [item], + }; + group.conditions[i] = newGroup; + group.connector = group.connector ?? "and"; + return; + } + + if (isConditionGroup(item)) { + createGroupFromResource(item, resourceId); + } + } +}; + +export const updateCondition = ( + group: TConditionGroup, + resourceId: string, + condition: Partial +) => { + for (let i = 0; i < group.conditions.length; i++) { + const item = group.conditions[i]; + + if (item.id === resourceId && !("connector" in item)) { + group.conditions[i] = { ...item, ...condition } as TSingleCondition; + return; + } + + if (isConditionGroup(item)) { + updateCondition(item, resourceId, condition); + } + } +}; + +export const getUpdatedActionBody = ( + action: TSurveyLogicAction, + objective: TActionObjective +): TSurveyLogicAction => { + if (objective === action.objective) return action; + switch (objective) { + case "calculate": + return { + id: action.id, + objective: "calculate", + variableId: "", + operator: "assign", + value: { type: "static", value: "" }, + }; + case "requireAnswer": + return { + id: action.id, + objective: "requireAnswer", + target: "", + }; + case "jumpToQuestion": + return { + id: action.id, + objective: "jumpToQuestion", + target: "", + }; + } +}; export const evaluateLogic = ( localSurvey: TSurvey, diff --git a/packages/surveys/src/components/general/Survey.tsx b/packages/surveys/src/components/general/Survey.tsx index e4c0595d32..c6983f6865 100644 --- a/packages/surveys/src/components/general/Survey.tsx +++ b/packages/surveys/src/components/general/Survey.tsx @@ -8,10 +8,10 @@ import { SurveyCloseButton } from "@/components/general/SurveyCloseButton"; import { WelcomeCard } from "@/components/general/WelcomeCard"; import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper"; import { StackedCardsContainer } from "@/components/wrappers/StackedCardsContainer"; -import { evaluateLogic, performActions } from "@/lib/logicEvaluator"; import { parseRecallInformation } from "@/lib/recall"; import { cn } from "@/lib/utils"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils"; import { SurveyBaseProps } from "@formbricks/types/formbricks-surveys"; import type { TResponseData, diff --git a/packages/surveys/src/lib/logicEvaluator.ts b/packages/surveys/src/lib/logicEvaluator.ts deleted file mode 100644 index 7ddfc17804..0000000000 --- a/packages/surveys/src/lib/logicEvaluator.ts +++ /dev/null @@ -1,459 +0,0 @@ -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { isConditionGroup } from "@formbricks/lib/survey/logic/utils"; -import { TResponseData, TResponseVariables } from "@formbricks/types/responses"; -import { - TActionCalculate, - TConditionGroup, - TSingleCondition, - TSurvey, - TSurveyLogicAction, - TSurveyQuestion, - TSurveyQuestionTypeEnum, - TSurveyVariable, -} from "@formbricks/types/surveys/types"; - -export const evaluateLogic = ( - localSurvey: TSurvey, - data: TResponseData, - variablesData: TResponseVariables, - conditions: TConditionGroup, - selectedLanguage: string -): boolean => { - const evaluateConditionGroup = (group: TConditionGroup): boolean => { - const results = group.conditions.map((condition) => { - if (isConditionGroup(condition)) { - return evaluateConditionGroup(condition); - } else { - return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage); - } - }); - - return group.connector === "or" ? results.some((r) => r) : results.every((r) => r); - }; - - return evaluateConditionGroup(conditions); -}; - -const evaluateSingleCondition = ( - localSurvey: TSurvey, - data: TResponseData, - variablesData: TResponseVariables, - condition: TSingleCondition, - selectedLanguage: string -): boolean => { - try { - let leftValue = getLeftOperandValue( - localSurvey, - data, - variablesData, - condition.leftOperand, - selectedLanguage - ); - let rightValue = condition.rightOperand - ? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand) - : undefined; - - 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 = ""; - } - - if ( - condition.leftOperand.type === "variable" && - (leftField as TSurveyVariable).type === "number" && - condition.rightOperand?.type === "hiddenField" - ) { - rightValue = Number(rightValue as string); - } - - switch (condition.operator) { - case "equals": - if (condition.leftOperand.type === "question") { - if ( - (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date && - typeof leftValue === "string" && - typeof rightValue === "string" - ) { - // when left value is of date question and right value is 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.MultipleChoiceMulti) { - if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { - return rightValue.includes(leftValue as string); - } else return false; - } 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 === rightValue - ); - case "doesNotEqual": - // when left value is of picture selection question and right value is its option - if ( - condition.leftOperand.type === "question" && - (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.PictureSelection && - 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.MultipleChoiceMulti) { - if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { - return !rightValue.includes(leftValue as string); - } else return false; - } 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 !== 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": - 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 -) => { - switch (leftOperand.type) { - case "question": - const currentQuestion = localSurvey.questions.find((q) => q.id === leftOperand.value); - if (!currentQuestion) return undefined; - - const responseValue = data[leftOperand.value]; - - if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") { - return Number(responseValue) || 0; - } - - if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") { - const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other"; - - if (typeof responseValue === "string") { - const choice = currentQuestion.choices.find((choice) => { - return getLocalizedValue(choice.label, selectedLanguage) === responseValue; - }); - - if (!choice) { - if (isOthersEnabled) { - return "other"; - } - - return undefined; - } - - return choice.id; - } else if (Array.isArray(responseValue)) { - let choice: string[] = []; - responseValue.forEach((value) => { - const foundChoice = currentQuestion.choices.find((choice) => { - return getLocalizedValue(choice.label, selectedLanguage) === value; - }); - - if (foundChoice) { - choice.push(foundChoice.id); - } else if (isOthersEnabled) { - choice.push("other"); - } - }); - if (choice) { - return Array.from(new Set(choice)); - } - } - } - - return data[leftOperand.value]; - case "variable": - const variables = localSurvey.variables || []; - const variable = variables.find((v) => v.id === leftOperand.value); - - if (!variable) return undefined; - - const variableValue = variablesData[leftOperand.value]; - - if (variable.type === "number") return Number(variableValue) || 0; - return variableValue || ""; - case "hiddenField": - return data[leftOperand.value]; - default: - return undefined; - } -}; - -const getRightOperandValue = ( - localSurvey: TSurvey, - data: TResponseData, - variablesData: TResponseVariables, - rightOperand: TSingleCondition["rightOperand"] -) => { - if (!rightOperand) return undefined; - - switch (rightOperand.type) { - case "question": - return data[rightOperand.value]; - case "variable": - const variables = localSurvey.variables || []; - const variable = variables.find((v) => v.id === rightOperand.value); - - if (!variable) return undefined; - - const variableValue = variablesData[rightOperand.value]; - - if (variable.type === "number") return Number(variableValue) || 0; - return variableValue || ""; - case "hiddenField": - return data[rightOperand.value]; - case "static": - return rightOperand.value; - default: - return undefined; - } -}; - -export const performActions = ( - survey: TSurvey, - actions: TSurveyLogicAction[], - 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; -};