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 111da7f383..11b0b8e2fc 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 @@ -105,7 +105,7 @@ const starRatingSurvey = (t: TFunction): TXMTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThanOrEqual", rightOperand: { @@ -195,7 +195,7 @@ const csatSurvey = (t: TFunction): TXMTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThanOrEqual", rightOperand: { @@ -316,7 +316,7 @@ const smileysRatingSurvey = (t: TFunction): TXMTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThanOrEqual", rightOperand: { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.test.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.test.ts index 8f78e3e2be..e38eec7463 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.test.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.test.ts @@ -325,7 +325,7 @@ describe("getSurveySummaryDropOff", () => { { id: "c1", leftOperand: { - type: "question" as const, + type: "element" as const, value: "q2", }, operator: "equals" as const, diff --git a/apps/web/app/lib/survey-block-builder.ts b/apps/web/app/lib/survey-block-builder.ts index ecf1e0b3f5..d48eabf828 100644 --- a/apps/web/app/lib/survey-block-builder.ts +++ b/apps/web/app/lib/survey-block-builder.ts @@ -227,7 +227,7 @@ export const createBlockJumpLogic = ( id: createId(), leftOperand: { value: sourceElementId, - type: "question", + type: "element", }, operator: operator, }, @@ -257,7 +257,7 @@ export const createBlockChoiceJumpLogic = ( id: createId(), leftOperand: { value: sourceElementId, - type: "question", + type: "element", }, operator: "equals", rightOperand: { diff --git a/apps/web/app/lib/survey-builder.ts b/apps/web/app/lib/survey-builder.ts index ec5263c5da..96f538b948 100644 --- a/apps/web/app/lib/survey-builder.ts +++ b/apps/web/app/lib/survey-builder.ts @@ -27,7 +27,7 @@ export const createJumpLogic = ( id: createId(), leftOperand: { value: sourceQuestionId, - type: "question", + type: "element", }, operator: operator, }, @@ -57,7 +57,7 @@ export const createChoiceJumpLogic = ( id: createId(), leftOperand: { value: sourceQuestionId, - type: "question", + type: "element", }, operator: "equals", rightOperand: { diff --git a/apps/web/app/lib/templates.ts b/apps/web/app/lib/templates.ts index f8c6084a6b..401f2ebee5 100644 --- a/apps/web/app/lib/templates.ts +++ b/apps/web/app/lib/templates.ts @@ -1078,7 +1078,7 @@ const reviewPrompt = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThanOrEqual", rightOperand: { @@ -1902,7 +1902,7 @@ const integrationSetupSurvey = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isGreaterThanOrEqual", rightOperand: { @@ -2347,7 +2347,7 @@ const collectFeedback = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThanOrEqual", rightOperand: { @@ -2391,7 +2391,7 @@ const collectFeedback = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[1], - type: "question", + type: "element", }, operator: "isSubmitted", }, @@ -3016,7 +3016,7 @@ const rateCheckoutExperience = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isGreaterThanOrEqual", rightOperand: { @@ -3111,7 +3111,7 @@ const measureSearchExperience = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isGreaterThanOrEqual", rightOperand: { @@ -3206,7 +3206,7 @@ const evaluateContentQuality = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isGreaterThanOrEqual", rightOperand: { @@ -3329,7 +3329,7 @@ const measureTaskAccomplishment = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[1], - type: "question", + type: "element", }, operator: "isGreaterThanOrEqual", rightOperand: { @@ -3372,7 +3372,7 @@ const measureTaskAccomplishment = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[2], - type: "question", + type: "element", }, operator: "isSubmitted", }, @@ -3380,7 +3380,7 @@ const measureTaskAccomplishment = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[1], - type: "question", + type: "element", }, operator: "isSkipped", }, @@ -3419,7 +3419,7 @@ const measureTaskAccomplishment = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[3], - type: "question", + type: "element", }, operator: "isSubmitted", }, @@ -3427,7 +3427,7 @@ const measureTaskAccomplishment = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[1], - type: "question", + type: "element", }, operator: "isSkipped", }, @@ -3534,7 +3534,7 @@ const identifySignUpBarriers = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[1], - type: "question", + type: "element", }, operator: "equals", rightOperand: { @@ -3848,7 +3848,7 @@ const improveNewsletterContent = (t: TFunction): TTemplate => { id: createId(), leftOperand: { value: reusableElementIds[0], - type: "question", + type: "element", }, operator: "isLessThan", rightOperand: { diff --git a/apps/web/lib/survey/__mock__/survey.mock.ts b/apps/web/lib/survey/__mock__/survey.mock.ts index 5dba172aaf..4aa58d598a 100644 --- a/apps/web/lib/survey/__mock__/survey.mock.ts +++ b/apps/web/lib/survey/__mock__/survey.mock.ts @@ -418,7 +418,7 @@ export const mockSurveyWithLogic: TSurvey = { conditions: [ { id: "swlje0bsnh6lkyk8vqs13oyr", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "equals", rightOperand: { type: "static", value: "blue" }, }, @@ -434,13 +434,13 @@ export const mockSurveyWithLogic: TSurvey = { conditions: [ { id: "n74oght3ozqgwm9rifp2fxrr", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "equals", rightOperand: { type: "static", value: "blue" }, }, { id: "fg4c9dwt9qjy8aba7zxbfdqd", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, operator: "equals", rightOperand: { type: "static", value: "pizza" }, }, @@ -456,13 +456,13 @@ export const mockSurveyWithLogic: TSurvey = { conditions: [ { id: "tmj7p9d3kpz1v4mcgpguqytw", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, operator: "equals", rightOperand: { type: "static", value: "pizza" }, }, { id: "rs7v5mmoetff7x8lo1gdsgpr", - leftOperand: { type: "question", value: "q3" }, + leftOperand: { type: "element", value: "q3" }, operator: "equals", rightOperand: { type: "static", value: "Inception" }, }, @@ -480,7 +480,7 @@ export const mockSurveyWithLogic: TSurvey = { id: "ddhaccfqy7rr3d5jdswl8yl8", leftOperand: { type: "variable", value: "siog1dabtpo3l0a3xoxw2922" }, operator: "equals", - rightOperand: { type: "question", value: "q4" }, + rightOperand: { type: "element", value: "q4" }, }, ], }, @@ -502,7 +502,7 @@ export const mockSurveyWithLogic: TSurvey = { id: "ot894j7nwna24i6jo2zpk59o", leftOperand: { type: "variable", value: "km1srr55owtn2r7lkoh5ny1u" }, operator: "isLessThan", - rightOperand: { type: "question", value: "q5" }, + rightOperand: { type: "element", value: "q5" }, }, ], }, @@ -516,7 +516,7 @@ export const mockSurveyWithLogic: TSurvey = { conditions: [ { id: "rb223vmzuuzo3ag1bp2m3i69", - leftOperand: { type: "question", value: "q6" }, + leftOperand: { type: "element", value: "q6" }, operator: "includesOneOf", rightOperand: { type: "static", @@ -525,7 +525,7 @@ export const mockSurveyWithLogic: TSurvey = { }, { id: "ot894j7nwna24i6jo2zpk59o", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "doesNotEqual", rightOperand: { type: "static", value: "teal" }, }, @@ -535,7 +535,7 @@ export const mockSurveyWithLogic: TSurvey = { conditions: [ { id: "gy6xowchkv8bp1qj7ur79jvc", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, operator: "doesNotEqual", rightOperand: { type: "static", value: "pizza" }, }, @@ -543,13 +543,13 @@ export const mockSurveyWithLogic: TSurvey = { id: "vxyccgwsbq34s3l0syom7y2w", leftOperand: { type: "hiddenField", value: "name" }, operator: "contains", - rightOperand: { type: "question", value: "q2" }, + rightOperand: { type: "element", value: "q2" }, }, ], }, { id: "yunz0k9w0xwparogz2n1twoy", - leftOperand: { type: "question", value: "q3" }, + leftOperand: { type: "element", value: "q3" }, operator: "doesNotEqual", rightOperand: { type: "static", value: "Inception" }, }, diff --git a/apps/web/lib/surveyLogic/utils.test.ts b/apps/web/lib/surveyLogic/utils.test.ts index 1241eb1724..2c2692ce7d 100644 --- a/apps/web/lib/surveyLogic/utils.test.ts +++ b/apps/web/lib/surveyLogic/utils.test.ts @@ -448,7 +448,7 @@ describe("surveyLogic", () => { mockSurvey, {}, vars, - group({ ...baseCond("equals", "foo"), leftOperand: { type: "question", value: "notfound" } }), + group({ ...baseCond("equals", "foo"), leftOperand: { type: "element", value: "notfound" } }), "en" ) ).toBe(false); @@ -854,7 +854,7 @@ describe("surveyLogic", () => { // Test number question const numberCondition: TSingleCondition = { id: "numCond", - leftOperand: { type: "question", value: "numQuestion" }, + leftOperand: { type: "element", value: "numQuestion" }, operator: "equals", rightOperand: { type: "static", value: 42 }, }; @@ -871,7 +871,7 @@ describe("surveyLogic", () => { // Test MC single with recognized choice const mcSingleCondition: TSingleCondition = { id: "mcCond", - leftOperand: { type: "question", value: "mcSingle" }, + leftOperand: { type: "element", value: "mcSingle" }, operator: "equals", rightOperand: { type: "static", value: "choice1" }, }; @@ -888,7 +888,7 @@ describe("surveyLogic", () => { // Test MC multi const mcMultiCondition: TSingleCondition = { id: "mcMultiCond", - leftOperand: { type: "question", value: "mcMulti" }, + leftOperand: { type: "element", value: "mcMulti" }, operator: "includesOneOf", rightOperand: { type: "static", value: ["choice1"] }, }; @@ -905,7 +905,7 @@ describe("surveyLogic", () => { // Test matrix question const matrixCondition: TSingleCondition = { id: "matrixCond", - leftOperand: { type: "question", value: "matrixQ", meta: { row: "0" } }, + leftOperand: { type: "element", value: "matrixQ", meta: { row: "0" } }, operator: "equals", rightOperand: { type: "static", value: "0" }, }; @@ -939,7 +939,7 @@ describe("surveyLogic", () => { // Test with missing question const missingQuestionCondition: TSingleCondition = { id: "missingCond", - leftOperand: { type: "question", value: "nonExistent" }, + leftOperand: { type: "element", value: "nonExistent" }, operator: "equals", rightOperand: { type: "static", value: "foo" }, }; @@ -973,7 +973,7 @@ describe("surveyLogic", () => { // Test MC single with "other" option const otherCondition: TSingleCondition = { id: "otherCond", - leftOperand: { type: "question", value: "mcSingle" }, + leftOperand: { type: "element", value: "mcSingle" }, operator: "equals", rightOperand: { type: "static", value: "Unknown option" }, }; @@ -990,7 +990,7 @@ describe("surveyLogic", () => { // Test matrix with invalid row index const invalidMatrixCondition: TSingleCondition = { id: "invalidMatrixCond", - leftOperand: { type: "question", value: "matrixQ", meta: { row: "999" } }, + leftOperand: { type: "element", value: "matrixQ", meta: { row: "999" } }, operator: "equals", rightOperand: { type: "static", value: "0" }, }; @@ -1049,7 +1049,7 @@ describe("surveyLogic", () => { id: "questionCond", leftOperand: { type: "hiddenField", value: "f" }, operator: "equals", - rightOperand: { type: "question", value: "question1" }, + rightOperand: { type: "element", value: "question1" }, }; const variableCondition: TSingleCondition = { @@ -1150,7 +1150,7 @@ describe("surveyLogic", () => { objective: "calculate", variableId: "numVar", operator: "add", - value: { type: "question", value: "questionNum" }, + value: { type: "element", value: "questionNum" }, }; // Test with hidden field value @@ -1168,7 +1168,7 @@ describe("surveyLogic", () => { objective: "calculate", variableId: "textVar", operator: "concat", - value: { type: "question", value: "questionText" }, + value: { type: "element", value: "questionText" }, }; // Test with missing variable @@ -1186,7 +1186,7 @@ describe("surveyLogic", () => { objective: "calculate", variableId: "numVar", operator: "add", - value: { type: "question", value: "nonExistentQuestion" }, + value: { type: "element", value: "nonExistentQuestion" }, }; // Test with other math operations @@ -1348,7 +1348,7 @@ describe("surveyLogic", () => { const condition: TSingleCondition = { id: "numCond", - leftOperand: { type: "question", value: "numQuestion" }, + leftOperand: { type: "element", value: "numQuestion" }, operator: "equals", rightOperand: { type: "static", value: 0 }, }; diff --git a/apps/web/lib/surveyLogic/utils.ts b/apps/web/lib/surveyLogic/utils.ts index 1a757ff429..0632c33868 100644 --- a/apps/web/lib/surveyLogic/utils.ts +++ b/apps/web/lib/surveyLogic/utils.ts @@ -272,7 +272,7 @@ const evaluateSingleCondition = ( let leftField: TSurveyElement | TSurveyVariable | string; - if (condition.leftOperand?.type === "question") { + if (condition.leftOperand?.type === "element") { leftField = questions.find((q) => q.id === condition.leftOperand?.value) ?? ""; } else if (condition.leftOperand?.type === "variable") { leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable; @@ -284,7 +284,7 @@ const evaluateSingleCondition = ( let rightField: TSurveyElement | TSurveyVariable | string; - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { rightField = questions.find((q) => q.id === condition.rightOperand?.value) ?? ""; } else if (condition.rightOperand?.type === "variable") { rightField = localSurvey.variables.find( @@ -306,7 +306,7 @@ const evaluateSingleCondition = ( switch (condition.operator) { case "equals": - if (condition.leftOperand.type === "question") { + if (condition.leftOperand.type === "element") { if ( (leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date && typeof leftValue === "string" && @@ -318,7 +318,7 @@ const evaluateSingleCondition = ( } // when left value is of openText, hiddenField, variable and right value is of multichoice - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) { if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { return rightValue.includes(leftValue as string); @@ -342,7 +342,7 @@ const evaluateSingleCondition = ( case "doesNotEqual": // when left value is of picture selection question and right value is its option if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.PictureSelection && Array.isArray(leftValue) && leftValue.length > 0 && @@ -353,7 +353,7 @@ const evaluateSingleCondition = ( // when left value is of date question and right value is string if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date && typeof leftValue === "string" && typeof rightValue === "string" @@ -362,7 +362,7 @@ const evaluateSingleCondition = ( } // when left value is of openText, hiddenField, variable and right value is of multichoice - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) { if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { return !rightValue.includes(leftValue as string); @@ -398,7 +398,7 @@ const evaluateSingleCondition = ( case "isSubmitted": if (typeof leftValue === "string") { if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.FileUpload && leftValue ) { @@ -511,7 +511,7 @@ const getLeftOperandValue = ( selectedLanguage: string ) => { switch (leftOperand.type) { - case "question": + case "element": const questions = getElementsFromBlocks(localSurvey.blocks); const currentQuestion = questions.find((q) => q.id === leftOperand.value); if (!currentQuestion) return undefined; @@ -609,7 +609,7 @@ const getRightOperandValue = ( if (!rightOperand) return undefined; switch (rightOperand.type) { - case "question": + case "element": return data[rightOperand.value]; case "variable": const variables = localSurvey.variables || []; @@ -685,7 +685,7 @@ const performCalculation = ( operandValue = value; } break; - case "question": + case "element": case "hiddenField": const val = data[action.value.value]; if (typeof val === "number" || typeof val === "string") { diff --git a/apps/web/lib/utils/recall.test.ts b/apps/web/lib/utils/recall.test.ts index 057bdd6456..3edc5dedea 100644 --- a/apps/web/lib/utils/recall.test.ts +++ b/apps/web/lib/utils/recall.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi } from "vitest"; import { TResponseData, TResponseVariables } from "@formbricks/types/responses"; -import { TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types"; import { structuredClone } from "@/lib/pollyfills/structuredClone"; import { checkForEmptyFallBackValue, @@ -40,7 +40,7 @@ vi.mock("@/lib/utils/datetime", () => ({ return false; } }), - formatDateWithOrdinal: vi.fn((date) => { + formatDateWithOrdinal: vi.fn(() => { return "January 1st, 2023"; }), })); @@ -355,10 +355,10 @@ describe("recall utility functions", () => { expect(result).toHaveLength(2); expect(result[0].id).toBe("id1"); expect(result[0].label).toBe("Question One"); - expect(result[0].type).toBe("question"); + expect(result[0].type).toBe("element"); expect(result[1].id).toBe("id2"); expect(result[1].label).toBe("Question Two"); - expect(result[1].type).toBe("question"); + expect(result[1].type).toBe("element"); }); test("handles hidden fields in recall items", () => { @@ -422,7 +422,7 @@ describe("recall utility functions", () => { describe("headlineToRecall", () => { test("transforms headlines to recall info", () => { const text = "What do you think of @Product?"; - const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "question" }]; + const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "element" }]; const fallbacks: fallbacks = { product: "our product", }; @@ -434,8 +434,8 @@ describe("recall utility functions", () => { test("transforms multiple headlines", () => { const text = "Rate @Product made by @Company"; const recallItems: TSurveyRecallItem[] = [ - { id: "product", label: "Product", type: "question" }, - { id: "company", label: "Company", type: "question" }, + { id: "product", label: "Product", type: "element" }, + { id: "company", label: "Company", type: "element" }, ]; const fallbacks: fallbacks = { product: "our product", diff --git a/apps/web/lib/utils/recall.ts b/apps/web/lib/utils/recall.ts index 8ea6e31222..bb75063a54 100644 --- a/apps/web/lib/utils/recall.ts +++ b/apps/web/lib/utils/recall.ts @@ -170,7 +170,7 @@ export const getRecallItems = (text: string, survey: TSurvey, languageCode: stri const getRecallItemType = () => { if (isHiddenField) return "hiddenField"; - if (isSurveyQuestion) return "question"; + if (isSurveyQuestion) return "element"; if (isVariable) return "variable"; }; diff --git a/apps/web/modules/ee/quotas/components/quota-modal.tsx b/apps/web/modules/ee/quotas/components/quota-modal.tsx index 018c44e931..a3922f4484 100644 --- a/apps/web/modules/ee/quotas/components/quota-modal.tsx +++ b/apps/web/modules/ee/quotas/components/quota-modal.tsx @@ -94,7 +94,7 @@ export const QuotaModal = ({ conditions: [ { id: createId(), - leftOperand: { type: "question", value: firstQuestion?.id }, + leftOperand: { type: "element", value: firstQuestion?.id }, operator: firstQuestion ? getDefaultOperatorForElement(firstQuestion, t) : "equals", }, ], diff --git a/apps/web/modules/ee/quotas/lib/evaluation-service.test.ts b/apps/web/modules/ee/quotas/lib/evaluation-service.test.ts index 1ddc138783..951200b6b1 100644 --- a/apps/web/modules/ee/quotas/lib/evaluation-service.test.ts +++ b/apps/web/modules/ee/quotas/lib/evaluation-service.test.ts @@ -134,7 +134,7 @@ describe("Quota Evaluation Service", () => { conditions: [ { id: "c1", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "isGreaterThanOrEqual", rightOperand: { type: "static", value: 18 }, }, diff --git a/apps/web/modules/ee/quotas/lib/utils.test.ts b/apps/web/modules/ee/quotas/lib/utils.test.ts index 1897b9fa2c..b84cce0853 100644 --- a/apps/web/modules/ee/quotas/lib/utils.test.ts +++ b/apps/web/modules/ee/quotas/lib/utils.test.ts @@ -126,13 +126,13 @@ describe("Quota Utils", () => { conditions: [ { id: "c1", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "isGreaterThanOrEqual", rightOperand: { type: "static", value: 18 }, }, { id: "c2", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "isLessThanOrEqual", rightOperand: { type: "static", value: 25 }, }, @@ -155,7 +155,7 @@ describe("Quota Utils", () => { conditions: [ { id: "c3", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, operator: "isGreaterThan", rightOperand: { type: "static", value: 25 }, }, diff --git a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx index b5a62c2d45..b9306e2dfb 100644 --- a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx +++ b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx @@ -124,7 +124,7 @@ export const RecallItemSelect = ({ ); }) .map((question) => { - return { id: question.id, label: question.headline[selectedLanguageCode], type: "question" as const }; + return { id: question.id, label: question.headline[selectedLanguageCode], type: "element" as const }; }); return filteredQuestions; @@ -143,7 +143,7 @@ export const RecallItemSelect = ({ const getRecallItemIcon = (recallItem: TSurveyRecallItem) => { switch (recallItem.type) { - case "question": + case "element": const question = questions.find((question) => question.id === recallItem.id); if (question) { return questionIconMapping[question?.type as keyof typeof questionIconMapping]; diff --git a/apps/web/modules/survey/editor/components/conditional-logic.tsx b/apps/web/modules/survey/editor/components/conditional-logic.tsx index ea1234d427..b80e368cde 100644 --- a/apps/web/modules/survey/editor/components/conditional-logic.tsx +++ b/apps/web/modules/survey/editor/components/conditional-logic.tsx @@ -75,7 +75,7 @@ export function ConditionalLogic({ id: createId(), leftOperand: { value: firstElement.id, - type: "question", + type: "element", }, operator, }, diff --git a/apps/web/modules/survey/editor/components/questions-view.tsx b/apps/web/modules/survey/editor/components/questions-view.tsx index 23047ecfdd..b9a2ec9243 100644 --- a/apps/web/modules/survey/editor/components/questions-view.tsx +++ b/apps/web/modules/survey/editor/components/questions-view.tsx @@ -140,7 +140,7 @@ export const QuestionsView = ({ updatedCondition.leftOperand = { ...condition.leftOperand, value: updatedId }; } - if (condition.rightOperand?.type === "question" && condition.rightOperand?.value === compareId) { + if (condition.rightOperand?.type === "element" && condition.rightOperand?.value === compareId) { updatedCondition.rightOperand = { ...condition.rightOperand, value: updatedId }; } diff --git a/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts b/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts index c50e261f56..c86d235578 100644 --- a/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts +++ b/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts @@ -69,7 +69,7 @@ vi.mock("@/lib/surveyLogic/utils", () => ({ vi.mock("@/modules/survey/editor/lib/utils", () => ({ getConditionValueOptions: vi.fn().mockReturnValue([ - { value: "question1", label: "Question 1", type: "question" }, + { value: "question1", label: "Question 1", type: "element" }, { value: "variable1", label: "Variable 1", type: "variable" }, ]), getConditionOperatorOptions: vi.fn().mockReturnValue([ @@ -88,6 +88,7 @@ vi.mock("@/modules/survey/editor/lib/utils", () => ({ { value: "isEmpty", label: "is empty" }, ]), getDefaultOperatorForQuestion: vi.fn().mockReturnValue("equals"), + getDefaultOperatorForElement: vi.fn().mockReturnValue("equals"), })); vi.mock("@paralleldrive/cuid2", () => ({ @@ -249,7 +250,7 @@ describe("shared-conditions-factory", () => { test("should call getConditionValueOptions with questionIdx", async () => { const paramsWithQuestionIdx = { ...defaultParams, - questionIdx: 0, + blockIdx: 0, }; const result = createSharedConditionsFactory(paramsWithQuestionIdx, defaultCallbacks); @@ -263,7 +264,7 @@ describe("shared-conditions-factory", () => { const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); const mockCondition: TSingleCondition = { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", }; @@ -276,12 +277,12 @@ describe("shared-conditions-factory", () => { test("should call getMatchValueProps with questionIdx", async () => { const paramsWithQuestionIdx = { ...defaultParams, - questionIdx: 0, + blockIdx: 0, }; const result = createSharedConditionsFactory(paramsWithQuestionIdx, defaultCallbacks); const mockCondition: TSingleCondition = { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", }; @@ -308,7 +309,7 @@ describe("shared-conditions-factory", () => { const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); const mockCondition: TSingleCondition = { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", }; @@ -390,9 +391,9 @@ describe("shared-conditions-factory", () => { const updates = { leftOperand: { value: "matrix-question.row1", - type: "question" as const, + type: "element" as const, meta: { - type: "question" as const, + type: "element" as const, }, }, }; @@ -407,7 +408,7 @@ describe("shared-conditions-factory", () => { conditions: [ { id: "condition1", - leftOperand: { value: "matrix-question", type: "question" }, + leftOperand: { value: "matrix-question", type: "element" }, operator: "equals", rightOperand: { value: "x", type: "static" }, } as TSingleCondition, @@ -416,7 +417,7 @@ describe("shared-conditions-factory", () => { const updated = updater(structuredClone(initial)); expect(updated.conditions[0]).toMatchObject({ operator: "isEmpty", - leftOperand: { value: "matrix-question", type: "question", meta: { row: "row1" } }, + leftOperand: { value: "matrix-question", type: "element", meta: { row: "row1" } }, rightOperand: undefined, }); }); @@ -427,9 +428,9 @@ describe("shared-conditions-factory", () => { const updates = { leftOperand: { value: "question1", - type: "question" as const, + type: "element" as const, meta: { - type: "question" as const, + type: "element" as const, }, }, }; @@ -445,9 +446,9 @@ describe("shared-conditions-factory", () => { const updates = { leftOperand: { value: "matrix-question", - type: "question" as const, + type: "element" as const, meta: { - type: "question" as const, + type: "element" as const, }, }, }; @@ -465,13 +466,13 @@ describe("shared-conditions-factory", () => { conditions: [ { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", rightOperand: { value: "test", type: "static" }, }, { id: "condition2", - leftOperand: { value: "question2", type: "question" }, + leftOperand: { value: "question2", type: "element" }, operator: "doesNotEqual", rightOperand: { value: "test2", type: "static" }, }, @@ -511,7 +512,7 @@ describe("shared-conditions-factory", () => { conditions: [ { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", rightOperand: { value: "test", type: "static" }, }, @@ -519,7 +520,7 @@ describe("shared-conditions-factory", () => { id: "condition2", leftOperand: { value: "matrix-question", - type: "question", + type: "element", meta: { row: "row1" }, }, operator: "isEmpty", @@ -534,7 +535,7 @@ describe("shared-conditions-factory", () => { conditions: [ { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", rightOperand: { value: "test", type: "static" }, }, @@ -542,7 +543,7 @@ describe("shared-conditions-factory", () => { id: "condition2", leftOperand: { value: "matrix-question", - type: "question", + type: "element", meta: { row: "row1" }, }, operator: "isEmpty", @@ -595,7 +596,7 @@ describe("shared-conditions-factory", () => { }); }); - test("should preserve meta for question type conditions", () => { + test("should preserve meta for element type conditions", () => { const genericConditions: TQuotaConditionGroup = { id: "root", connector: "and", @@ -604,7 +605,7 @@ describe("shared-conditions-factory", () => { id: "condition1", leftOperand: { value: "question1", - type: "question" as const, + type: "element" as const, meta: { row: "row1", column: "col1" }, }, operator: "equals", @@ -616,7 +617,7 @@ describe("shared-conditions-factory", () => { const result = genericConditionsToQuota(genericConditions); expect(result.conditions[0].leftOperand).toHaveProperty("meta"); - if (result.conditions[0].leftOperand.type === "question") { + if (result.conditions[0].leftOperand.type === "element") { expect(result.conditions[0].leftOperand.meta).toEqual({ row: "row1", column: "col1" }); } }); @@ -651,7 +652,7 @@ describe("shared-conditions-factory", () => { conditions: [ { id: "condition1", - leftOperand: { value: "question1", type: "question" }, + leftOperand: { value: "question1", type: "element" }, operator: "equals", rightOperand: { value: "test", type: "static" }, }, diff --git a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts index a3e11b6529..07c331ba9c 100644 --- a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts +++ b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts @@ -61,7 +61,7 @@ export function createSharedConditionsFactory( // Handles special update logic for matrix elements, setting appropriate operators and metadata const handleMatrixElementUpdate = (resourceId: string, updates: Partial): boolean => { - if (updates.leftOperand && updates.leftOperand.type === "question") { + if (updates.leftOperand && updates.leftOperand.type === "element") { const [elementId, rowId] = updates.leftOperand.value.split("."); const element = elements.find((q) => q.id === elementId); @@ -73,7 +73,7 @@ export function createSharedConditionsFactory( updateCondition(conditionsCopy, resourceId, { leftOperand: { value: elementId, - type: "question", + type: "element", meta: { row: rowId, }, @@ -112,7 +112,7 @@ export function createSharedConditionsFactory( const defaultOperator = elements.length > 0 ? getDefaultOperatorForElement(elements[0], t) : "equals"; const newCondition: TSingleCondition = { id: createId(), - leftOperand: { value: defaultLeftOperandValue, type: "question" }, + leftOperand: { value: defaultLeftOperandValue, type: "element" }, operator: defaultOperator, }; @@ -149,7 +149,7 @@ export function createSharedConditionsFactory( } // Check if the operator is correct for the element - if (updates.leftOperand?.type === "question" && updates.operator) { + if (updates.leftOperand?.type === "element" && updates.operator) { const elementId = updates.leftOperand.value.split(".")[0]; const element = elements.find((q) => q.id === elementId); @@ -219,7 +219,7 @@ export const genericConditionsToQuota = (genericConditions: TQuotaConditionGroup leftOperand: { type: leftOperand.type, value: leftOperand.value, - ...(leftOperand.type === "question" && leftOperand.meta && { meta: leftOperand.meta }), + ...(leftOperand.type === "element" && leftOperand.meta && { meta: leftOperand.meta }), }, operator: condition.operator, rightOperand: condition.rightOperand, diff --git a/apps/web/modules/survey/editor/lib/utils.tsx b/apps/web/modules/survey/editor/lib/utils.tsx index 4dd6c4e5aa..5ebb8c95b6 100644 --- a/apps/web/modules/survey/editor/lib/utils.tsx +++ b/apps/web/modules/survey/editor/lib/utils.tsx @@ -143,7 +143,7 @@ export const getConditionValueOptions = ( label: `${getTextContent(processedLabel.default ?? "")} (${elementHeadline})`, value: `${element.id}.${rowIdx}`, meta: { - type: "question", + type: "element", rowIdx: rowIdx.toString(), }, }; @@ -154,7 +154,7 @@ export const getConditionValueOptions = ( label: elementHeadline, value: element.id, meta: { - type: "question", + type: "element", }, children: [ { @@ -166,7 +166,7 @@ export const getConditionValueOptions = ( label: t("environments.surveys.edit.matrix_all_fields", "All fields"), value: element.id, meta: { - type: "question", + type: "element", }, }, ], @@ -179,7 +179,7 @@ export const getConditionValueOptions = ( ), value: element.id, meta: { - type: "question", + type: "element", }, }); } @@ -266,7 +266,7 @@ export const getElementOperatorOptions = ( options = getLogicRules(t).question[`openText.${inputType}`].options; } else if (element.type === TSurveyElementTypeEnum.Matrix && condition) { const isMatrixRow = - condition.leftOperand.type === "question" && condition.leftOperand?.meta?.row !== undefined; + condition.leftOperand.type === "element" && condition.leftOperand?.meta?.row !== undefined; options = getLogicRules(t).question[`matrix${isMatrixRow ? ".row" : ""}`].options; } else { options = getLogicRules(t).question[element.type].options; @@ -289,7 +289,7 @@ export const getDefaultOperatorForElement = ( }; export const getFormatLeftOperandValue = (condition: TSingleCondition, localSurvey: TSurvey): string => { - if (condition.leftOperand.type === "question") { + if (condition.leftOperand.type === "element") { const questions = getElementsFromBlocks(localSurvey.blocks); const question = questions.find((q) => q.id === condition.leftOperand.value); if (question && question.type === TSurveyElementTypeEnum.Matrix) { @@ -313,7 +313,7 @@ export const getConditionOperatorOptions = ( return getLogicRules(t)[`variable.${variableType}`].options; } else if (condition.leftOperand.type === "hiddenField") { return getLogicRules(t).hiddenField.options; - } else if (condition.leftOperand.type === "question") { + } else if (condition.leftOperand.type === "element") { // Derive questions from blocks const elements = getElementsFromBlocks(localSurvey.blocks); const element = elements.find((question) => { @@ -376,7 +376,7 @@ export const getMatchValueProps = ( const selectedElement = elements.find((element) => element.id === condition.leftOperand.value); const selectedVariable = variables.find((variable) => variable.id === condition.leftOperand.value); - if (condition.leftOperand.type === "question") { + if (condition.leftOperand.type === "element") { elements = elements.filter((element) => element.id !== condition.leftOperand.value); } else if (condition.leftOperand.type === "variable") { variables = variables.filter((variable) => variable.id !== condition.leftOperand.value); @@ -384,7 +384,7 @@ export const getMatchValueProps = ( hiddenFields = hiddenFields.filter((field) => field !== condition.leftOperand.value); } - if (condition.leftOperand.type === "question") { + if (condition.leftOperand.type === "element") { if (selectedElement?.type === TSurveyElementTypeEnum.OpenText) { const allowedElementTypes = [TSurveyElementTypeEnum.OpenText]; @@ -412,7 +412,7 @@ export const getMatchValueProps = ( ), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -629,7 +629,7 @@ export const getMatchValueProps = ( ), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -727,7 +727,7 @@ export const getMatchValueProps = ( label: getTextContent(processedHeadline.default ?? ""), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -802,7 +802,7 @@ export const getMatchValueProps = ( label: getTextContent(processedHeadline.default ?? ""), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -883,7 +883,7 @@ export const getMatchValueProps = ( label: getTextContent(processedHeadline.default ?? ""), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -1126,7 +1126,7 @@ export const getActionValueOptions = ( label: getTextContent(processedHeadline.default ?? ""), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -1184,7 +1184,7 @@ export const getActionValueOptions = ( label: getTextContent(getLocalizedValue(element.headline, "default")), value: element.id, meta: { - type: "question", + type: "element", }, }; }); @@ -1236,12 +1236,12 @@ export const getActionValueOptions = ( const isUsedInLeftOperand = ( leftOperand: TLeftOperand, - type: "question" | "hiddenField" | "variable", + type: "element" | "hiddenField" | "variable", id: string ): boolean => { switch (type) { - case "question": - return leftOperand.type === "question" && leftOperand.value === id; + case "element": + return leftOperand.type === "element" && leftOperand.value === id; case "hiddenField": return leftOperand.type === "hiddenField" && leftOperand.value === id; case "variable": @@ -1253,12 +1253,12 @@ const isUsedInLeftOperand = ( const isUsedInRightOperand = ( rightOperand: TRightOperand, - type: "question" | "hiddenField" | "variable", + type: "element" | "hiddenField" | "variable", id: string ): boolean => { switch (type) { - case "question": - return rightOperand.type === "question" && rightOperand.value === id; + case "element": + return rightOperand.type === "element" && rightOperand.value === id; case "hiddenField": return rightOperand.type === "hiddenField" && rightOperand.value === id; case "variable": @@ -1283,8 +1283,8 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues } else { // It's a TSingleCondition return ( - (condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "question", questionId)) || - isUsedInLeftOperand(condition.leftOperand, "question", questionId) + (condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", questionId)) || + isUsedInLeftOperand(condition.leftOperand, "element", questionId) ); } }; @@ -1335,8 +1335,8 @@ export const isUsedInQuota = ( if (questionId) { return quota.logic.conditions.some( (condition) => - (condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "question", questionId)) || - isUsedInLeftOperand(condition.leftOperand, "question", questionId) + (condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", questionId)) || + isUsedInLeftOperand(condition.leftOperand, "element", questionId) ); } @@ -1447,7 +1447,7 @@ export const findOptionUsedInLogic = ( }; const isUsedInOperand = (condition: TSingleCondition): boolean => { - if (condition.leftOperand.type === "question" && condition.leftOperand.value === questionId) { + if (condition.leftOperand.type === "element" && condition.leftOperand.value === questionId) { if (checkInLeftOperand) { if (condition.leftOperand.meta && Object.entries(condition.leftOperand.meta).length > 0) { const optionIdInMeta = Object.values(condition.leftOperand.meta).some( diff --git a/apps/web/modules/ui/components/editor/components/recall-node.tsx b/apps/web/modules/ui/components/editor/components/recall-node.tsx index c423aedf0e..89d5930bef 100644 --- a/apps/web/modules/ui/components/editor/components/recall-node.tsx +++ b/apps/web/modules/ui/components/editor/components/recall-node.tsx @@ -96,7 +96,7 @@ export class RecallNode extends DecoratorNode { constructor(payload?: RecallPayload, key?: NodeKey) { super(key); const defaultPayload: RecallPayload = { - recallItem: { id: "", label: "", type: "question" }, + recallItem: { id: "", label: "", type: "element" }, fallbackValue: "", }; const actualPayload = payload || defaultPayload; diff --git a/packages/database/migration/20251118032116_migrate_questions_to_blocks/migration.ts b/packages/database/migration/20251118032116_migrate_questions_to_blocks/migration.ts index f2690b38d4..5ca949f519 100644 --- a/packages/database/migration/20251118032116_migrate_questions_to_blocks/migration.ts +++ b/packages/database/migration/20251118032116_migrate_questions_to_blocks/migration.ts @@ -20,18 +20,37 @@ interface SurveyQuestion { [key: string]: unknown; } -interface Condition { +// Single condition type (leaf node) +interface SingleCondition { id: string; - leftOperand?: { value: string; type: string; meta?: Record }; - operator?: string; + leftOperand: { value: string; type: string; meta?: Record }; + operator: string; rightOperand?: { type: string; value: string | number | string[] }; - conditions?: Condition[]; - connector?: string; + connector?: undefined; // Single conditions don't have connectors } +// Condition group type (has nested conditions) +interface ConditionGroup { + id: string; + connector: "and" | "or"; + conditions: Condition[]; +} + +// Union type for both +type Condition = SingleCondition | ConditionGroup; + +// Type guards +const isSingleCondition = (condition: Condition): condition is SingleCondition => { + return "leftOperand" in condition && "operator" in condition; +}; + +const isConditionGroup = (condition: Condition): condition is ConditionGroup => { + return "conditions" in condition && "connector" in condition; +}; + interface SurveyLogic { id: string; - conditions: Condition; + conditions: ConditionGroup; // Logic always starts with a condition group actions: LogicAction[]; } @@ -74,6 +93,7 @@ interface CTAMigrationStats { /** * Check if a condition references a CTA element with a specific operator + * Can handle both SingleCondition and ConditionGroup */ const conditionReferencesCTA = ( condition: Condition | null | undefined, @@ -82,14 +102,19 @@ const conditionReferencesCTA = ( ): boolean => { if (!condition) return false; - if (condition.leftOperand?.value === ctaElementId) { - if (operator) { - return condition.operator === operator; + // Check if it's a single condition + if (isSingleCondition(condition)) { + if (condition.leftOperand.value === ctaElementId) { + if (operator) { + return condition.operator === operator; + } + return true; } - return true; + return false; } - if (condition.conditions && Array.isArray(condition.conditions)) { + // It's a condition group - check nested conditions + if (isConditionGroup(condition)) { return condition.conditions.some((c) => conditionReferencesCTA(c, ctaElementId, operator)); } @@ -100,23 +125,28 @@ const conditionReferencesCTA = ( * Remove conditions that reference a CTA element with specific operators */ const removeCtaConditions = ( - conditionGroup: Condition, + conditionGroup: ConditionGroup, ctaElementId: string, operatorsToRemove: string[] -): Condition | null => { - if (!conditionGroup.conditions) return conditionGroup; - +): ConditionGroup | null => { const filteredConditions = conditionGroup.conditions.filter((condition) => { - if (condition.leftOperand?.value === ctaElementId && condition.operator) { - return !operatorsToRemove.includes(condition.operator); + // Check if it's a single condition referencing the CTA + if (isSingleCondition(condition)) { + if (condition.leftOperand.value === ctaElementId) { + return !operatorsToRemove.includes(condition.operator); + } + return true; } - if (condition.conditions && Array.isArray(condition.conditions)) { + // It's a condition group - recurse + if (isConditionGroup(condition)) { const cleaned = removeCtaConditions(condition, ctaElementId, operatorsToRemove); - if (!cleaned?.conditions || cleaned.conditions.length === 0) { + if (!cleaned || cleaned.conditions.length === 0) { return false; } + // Replace the condition with the cleaned version Object.assign(condition, cleaned); + return true; } return true; @@ -333,6 +363,53 @@ const updateLogicFallback = ( return undefined; }; +/** + * Convert logic operand types from "question" to "element" recursively (immutable) + * @param condition - Condition or condition group to convert + * @returns New condition object with "element" type instead of "question" + */ +const convertQuestionToElementType = (condition: Condition | null | undefined): Condition | null => { + if (!condition) return null; + + // Handle single condition + if (isSingleCondition(condition)) { + const newCondition: SingleCondition = { ...condition }; + + // Update leftOperand if it's of type "question" + if (condition.leftOperand.type === "question") { + newCondition.leftOperand = { + ...condition.leftOperand, + type: "element", + }; + } + + // Update rightOperand if it exists and is of type "question" + if (condition.rightOperand && condition.rightOperand.type === "question") { + newCondition.rightOperand = { + ...condition.rightOperand, + type: "element", + }; + } + + return newCondition; + } + + // Handle condition group + if (isConditionGroup(condition)) { + const newConditionGroup: ConditionGroup = { + ...condition, + conditions: condition.conditions.map((nestedCondition) => { + const converted = convertQuestionToElementType(nestedCondition); + return converted ?? nestedCondition; + }), + }; + + return newConditionGroup; + } + + return null; +}; + /** * Migrate a survey from questions to blocks structure * Each question becomes a block with a single element @@ -384,10 +461,22 @@ const migrateQuestionsSurveyToBlocks = ( // Phase 2: Update all logic references for (const block of blocks) { if (block.logic && block.logic.length > 0) { - block.logic = block.logic.map((item) => ({ - ...item, - actions: updateLogicActions(item.actions, questionIdToBlockId, endingIds), - })); + block.logic = block.logic.map((item) => { + // Convert "question" type to "element" type in conditions (immutably) + const updatedConditions = convertQuestionToElementType(item.conditions); + + // Since item.conditions is always a ConditionGroup, the result should be too + if (!updatedConditions || !isConditionGroup(updatedConditions)) { + // This should never happen, but if it does, keep the original + return item; + } + + return { + ...item, + conditions: updatedConditions, + actions: updateLogicActions(item.actions, questionIdToBlockId, endingIds), + }; + }); } if (block.logicFallback) { diff --git a/packages/surveys/src/lib/logic.test.ts b/packages/surveys/src/lib/logic.test.ts index 32b8bae94f..57e9fe0d04 100644 --- a/packages/surveys/src/lib/logic.test.ts +++ b/packages/surveys/src/lib/logic.test.ts @@ -166,7 +166,7 @@ describe("Survey Logic", () => { const singleCondition: TSingleCondition = { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test" }, }; expect(isConditionGroup(singleCondition)).toBe(false); @@ -199,13 +199,13 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test answer" }, }, { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: 42 }, }, ], @@ -222,13 +222,13 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "wrong answer" }, }, { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: 42 }, }, ], @@ -245,7 +245,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test answer" }, }, { @@ -255,7 +255,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "wrong" }, }, { @@ -280,13 +280,13 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test answer" }, }, { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "wrong value" }, }, ], @@ -303,13 +303,13 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "wrong answer" }, }, { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "wrong value" }, }, ], @@ -508,7 +508,7 @@ describe("Survey Logic", () => { objective: "calculate", variableId: "var2", operator: "add", - value: { type: "question", value: "q2" }, + value: { type: "element", value: "q2" }, }, ]; @@ -609,7 +609,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "contains", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test" }, }, ], @@ -623,7 +623,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "doesNotContain", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "invalid" }, }, ], @@ -639,7 +639,7 @@ describe("Survey Logic", () => { { id: "condition3", operator: "startsWith", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test" }, }, ], @@ -655,7 +655,7 @@ describe("Survey Logic", () => { { id: "condition4", operator: "doesNotStartWith", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "invalid" }, }, ], @@ -671,7 +671,7 @@ describe("Survey Logic", () => { { id: "condition5", operator: "endsWith", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "answer" }, }, ], @@ -685,7 +685,7 @@ describe("Survey Logic", () => { { id: "condition6", operator: "doesNotEndWith", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "invalid" }, }, ], @@ -704,7 +704,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isGreaterThan", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "30" }, }, ], @@ -720,7 +720,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isLessThan", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "50" }, }, ], @@ -734,7 +734,7 @@ describe("Survey Logic", () => { { id: "condition3", operator: "isGreaterThanOrEqual", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "42" }, }, ], @@ -750,7 +750,7 @@ describe("Survey Logic", () => { { id: "condition4", operator: "isLessThanOrEqual", - leftOperand: { type: "question", value: "q2" }, + leftOperand: { type: "element", value: "q2" }, rightOperand: { type: "static", value: "42" }, }, ], @@ -769,7 +769,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isAfter", - leftOperand: { type: "question", value: "q5" }, + leftOperand: { type: "element", value: "q5" }, rightOperand: { type: "static", value: "2022-12-31" }, }, ], @@ -783,7 +783,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isBefore", - leftOperand: { type: "question", value: "q5" }, + leftOperand: { type: "element", value: "q5" }, rightOperand: { type: "static", value: "2023-01-02" }, }, ], @@ -797,7 +797,7 @@ describe("Survey Logic", () => { { id: "condition3", operator: "equals", - leftOperand: { type: "question", value: "q5" }, + leftOperand: { type: "element", value: "q5" }, rightOperand: { type: "static", value: "2023-01-01" }, }, ], @@ -816,7 +816,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "includesAllOf", - leftOperand: { type: "question", value: "q4" }, + leftOperand: { type: "element", value: "q4" }, rightOperand: { type: "static", value: ["opt1", "opt2"] }, }, ], @@ -832,7 +832,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "includesOneOf", - leftOperand: { type: "question", value: "q4" }, + leftOperand: { type: "element", value: "q4" }, rightOperand: { type: "static", value: ["opt1", "Invalid Option"] }, }, ], @@ -848,7 +848,7 @@ describe("Survey Logic", () => { { id: "condition3", operator: "doesNotIncludeAllOf", - leftOperand: { type: "question", value: "q4" }, + leftOperand: { type: "element", value: "q4" }, rightOperand: { type: "static", value: ["Invalid 1", "Invalid 2"] }, }, ], @@ -864,7 +864,7 @@ describe("Survey Logic", () => { { id: "condition4", operator: "doesNotIncludeOneOf", - leftOperand: { type: "question", value: "q4" }, + leftOperand: { type: "element", value: "q4" }, rightOperand: { type: "static", value: ["opt3", "Invalid Option"] }, }, ], @@ -883,7 +883,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isSubmitted", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, }, ], }; @@ -898,7 +898,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isSkipped", - leftOperand: { type: "question", value: "emptyField" }, + leftOperand: { type: "element", value: "emptyField" }, }, ], }; @@ -913,7 +913,7 @@ describe("Survey Logic", () => { { id: "condition3", operator: "isBooked", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, }, ], }; @@ -1009,7 +1009,7 @@ describe("Survey Logic", () => { id: "condition1", operator: "equals", leftOperand: { - type: "question", + type: "element", value: "q8", meta: { row: "0" }, }, @@ -1028,7 +1028,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isSubmitted", - leftOperand: { type: "question", value: "q6" }, + leftOperand: { type: "element", value: "q6" }, }, ], }; @@ -1043,7 +1043,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isSkipped", - leftOperand: { type: "question", value: "skippedUpload" }, + leftOperand: { type: "element", value: "skippedUpload" }, }, ], }; @@ -1064,7 +1064,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isPartiallySubmitted", - leftOperand: { type: "question", value: "q8" }, + leftOperand: { type: "element", value: "q8" }, }, ], }; @@ -1089,7 +1089,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isCompletelySubmitted", - leftOperand: { type: "question", value: "q8" }, + leftOperand: { type: "element", value: "q8" }, }, ], }; @@ -1114,7 +1114,7 @@ describe("Survey Logic", () => { id: "condition1", // @ts-ignore - intentionally using invalid operator for test operator: "invalidOperator", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test" }, }, ], @@ -1129,7 +1129,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "nonExistentId" }, + leftOperand: { type: "element", value: "nonExistentId" }, rightOperand: { type: "static", value: "test" }, }, ], @@ -1172,7 +1172,7 @@ describe("Survey Logic", () => { id: "condition1", operator: "equals", leftOperand: { - type: "question", + type: "element", value: "q8", meta: { row: "invalid-row" }, }, @@ -1192,7 +1192,7 @@ describe("Survey Logic", () => { id: "condition1", operator: "equals", leftOperand: { - type: "question", + type: "element", value: "q8", meta: { row: "99" }, // Invalid row index }, @@ -1216,7 +1216,7 @@ describe("Survey Logic", () => { id: "condition1", operator: "isEmpty", leftOperand: { - type: "question", + type: "element", value: "q8", meta: { row: "0" }, }, @@ -1236,7 +1236,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "doesNotEqual", - leftOperand: { type: "question", value: "q7" }, + leftOperand: { type: "element", value: "q7" }, rightOperand: { type: "static", value: "option2" }, }, ], @@ -1259,8 +1259,8 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "dateQ1" }, - rightOperand: { type: "question", value: "dateQ2" }, + leftOperand: { type: "element", value: "dateQ1" }, + rightOperand: { type: "element", value: "dateQ2" }, }, ], }; @@ -1305,8 +1305,8 @@ describe("Survey Logic", () => { { id: "condition1", operator: "doesNotEqual", - leftOperand: { type: "question", value: "dateQ1" }, - rightOperand: { type: "question", value: "dateQ2" }, + leftOperand: { type: "element", value: "dateQ1" }, + rightOperand: { type: "element", value: "dateQ2" }, }, ], }; @@ -1353,7 +1353,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "multiValue" }, + leftOperand: { type: "element", value: "multiValue" }, rightOperand: { type: "static", value: "option1" }, }, ], @@ -1370,8 +1370,8 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "q1" }, - rightOperand: { type: "question", value: "multiQ" }, + leftOperand: { type: "element", value: "q1" }, + rightOperand: { type: "element", value: "multiQ" }, }, ], }; @@ -1392,7 +1392,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isEmpty", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, }, ], }; @@ -1408,7 +1408,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isNotEmpty", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, }, ], }; @@ -1425,7 +1425,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "isAnyOf", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: ["wrong answer", "test answer", "another answer"] }, }, ], @@ -1440,7 +1440,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "isAnyOf", - leftOperand: { type: "question", value: "q1" }, + leftOperand: { type: "element", value: "q1" }, rightOperand: { type: "static", value: "test answer" }, }, ], @@ -1482,7 +1482,7 @@ describe("Survey Logic", () => { { id: "condition1", operator: "equals", - leftOperand: { type: "question", value: "multiChoiceWithOther" }, + leftOperand: { type: "element", value: "multiChoiceWithOther" }, rightOperand: { type: "static", value: "Custom Option" }, }, ], @@ -1503,7 +1503,7 @@ describe("Survey Logic", () => { { id: "condition2", operator: "equals", - leftOperand: { type: "question", value: "multiChoiceWithOther" }, + leftOperand: { type: "element", value: "multiChoiceWithOther" }, rightOperand: { type: "static", value: "opt1" }, }, ], diff --git a/packages/surveys/src/lib/logic.ts b/packages/surveys/src/lib/logic.ts index 733a3a82d6..f993b2c13f 100644 --- a/packages/surveys/src/lib/logic.ts +++ b/packages/surveys/src/lib/logic.ts @@ -88,7 +88,7 @@ const getLeftOperandValue = ( selectedLanguage: string ) => { switch (leftOperand.type) { - case "question": + case "element": const questions = getElementsFromSurvey(localSurvey); const currentQuestion = questions.find((q) => q.id === leftOperand.value); if (!currentQuestion) return undefined; @@ -188,7 +188,7 @@ const getRightOperandValue = ( if (!rightOperand) return undefined; switch (rightOperand.type) { - case "question": + case "element": return data[rightOperand.value]; case "variable": const variables = localSurvey.variables || []; @@ -224,7 +224,7 @@ const evaluateSingleCondition = ( let leftField: TSurveyElement | TSurveyVariable | string; const questions = getElementsFromSurvey(localSurvey); - if (condition.leftOperand?.type === "question") { + if (condition.leftOperand?.type === "element") { leftField = questions.find((q) => q.id === condition.leftOperand?.value) ?? ""; } else if (condition.leftOperand?.type === "variable") { leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable; @@ -236,7 +236,7 @@ const evaluateSingleCondition = ( let rightField: TSurveyElement | TSurveyVariable | string; - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { rightField = questions.find((q) => q.id === condition.rightOperand?.value) ?? ""; } else if (condition.rightOperand?.type === "variable") { rightField = localSurvey.variables.find( @@ -258,7 +258,7 @@ const evaluateSingleCondition = ( switch (condition.operator) { case "equals": - if (condition.leftOperand.type === "question") { + if (condition.leftOperand.type === "element") { if ( (leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date && typeof leftValue === "string" && @@ -270,7 +270,7 @@ const evaluateSingleCondition = ( } // when left value is of openText, hiddenField, variable and right value is of multichoice - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) { if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { return rightValue.includes(leftValue as string); @@ -294,7 +294,7 @@ const evaluateSingleCondition = ( case "doesNotEqual": // when left value is of picture selection question and right value is its option if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.PictureSelection && Array.isArray(leftValue) && leftValue.length > 0 && @@ -305,7 +305,7 @@ const evaluateSingleCondition = ( // when left value is of date question and right value is string if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date && typeof leftValue === "string" && typeof rightValue === "string" @@ -314,7 +314,7 @@ const evaluateSingleCondition = ( } // when left value is of openText, hiddenField, variable and right value is of multichoice - if (condition.rightOperand?.type === "question") { + if (condition.rightOperand?.type === "element") { if ((rightField as TSurveyElement).type === TSurveyElementTypeEnum.MultipleChoiceMulti) { if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { return !rightValue.includes(leftValue as string); @@ -350,7 +350,7 @@ const evaluateSingleCondition = ( case "isSubmitted": if (typeof leftValue === "string") { if ( - condition.leftOperand.type === "question" && + condition.leftOperand.type === "element" && (leftField as TSurveyElement).type === TSurveyElementTypeEnum.FileUpload && leftValue ) { @@ -474,7 +474,7 @@ const performCalculation = ( operandValue = value; } break; - case "question": + case "element": case "hiddenField": const val = data[action.value.value]; if (typeof val === "number" || typeof val === "string") { diff --git a/packages/types/surveys/logic.ts b/packages/types/surveys/logic.ts index b0b2e64362..027e2bf1a7 100644 --- a/packages/types/surveys/logic.ts +++ b/packages/types/surveys/logic.ts @@ -57,9 +57,9 @@ export const ZConnector = z.enum(["and", "or"]); export type TConnector = z.infer; // Dynamic field types for conditions -const ZDynamicQuestion = z.object({ - type: z.literal("question"), - value: z.string().min(1, "Conditional Logic: Question id cannot be empty"), +const ZDynamicElement = z.object({ + type: z.literal("element"), + value: z.string().min(1, "Conditional Logic: Element id cannot be empty"), meta: z.record(z.string()).optional(), }); @@ -76,7 +76,7 @@ const ZDynamicHiddenField = z.object({ value: z.string().min(1, "Conditional Logic: Hidden field id cannot be empty"), }); -export const ZDynamicLogicFieldValue = z.union([ZDynamicQuestion, ZDynamicVariable, ZDynamicHiddenField], { +export const ZDynamicLogicFieldValue = z.union([ZDynamicElement, ZDynamicVariable, ZDynamicHiddenField], { message: "Conditional Logic: Invalid dynamic field value", }); diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts index 7457533adb..04c051de1b 100644 --- a/packages/types/surveys/types.ts +++ b/packages/types/surveys/types.ts @@ -2000,7 +2000,7 @@ const validateConditions = ( const { leftOperand, operator, rightOperand } = condition; // Validate left operand - if (leftOperand.type === "question") { + if (leftOperand.type === "element") { const questionId = leftOperand.value; const questionIdx = survey.questions.findIndex((q) => q.id === questionId); const question = questionIdx !== -1 ? survey.questions[questionIdx] : undefined; @@ -2057,7 +2057,7 @@ const validateConditions = ( if (question.type === TSurveyQuestionTypeEnum.OpenText) { // Validate right operand - if (rightOperand?.type === "question") { + if (rightOperand?.type === "element") { const quesId = rightOperand.value; const ques = survey.questions.find((q) => q.id === quesId); @@ -2289,7 +2289,7 @@ const validateConditions = ( }); } } else if (question.type === TSurveyQuestionTypeEnum.Date) { - if (rightOperand?.type === "question") { + if (rightOperand?.type === "element") { const quesId = rightOperand.value; const ques = survey.questions.find((q) => q.id === quesId); @@ -2419,7 +2419,7 @@ const validateConditions = ( } // Validate right operand - if (rightOperand?.type === "question") { + if (rightOperand?.type === "element") { const questionId = rightOperand.value; const question = survey.questions.find((q) => q.id === questionId); @@ -2516,7 +2516,7 @@ const validateConditions = ( } // Validate right operand - if (rightOperand?.type === "question") { + if (rightOperand?.type === "element") { const questionId = rightOperand.value; const question = survey.questions.find((q) => q.id === questionId); @@ -2638,7 +2638,7 @@ const validateActions = ( }; } - if (action.value.type === "question") { + if (action.value.type === "element") { const allowedQuestions = [ TSurveyQuestionTypeEnum.OpenText, TSurveyQuestionTypeEnum.MultipleChoiceSingle, @@ -2670,7 +2670,7 @@ const validateActions = ( }; } - if (action.value.type === "question") { + if (action.value.type === "element") { const allowedQuestions = [TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS]; const selectedQuestion = previousQuestions.find((q) => q.id === action.value.value); @@ -2957,7 +2957,7 @@ const validateBlockConditions = ( const { leftOperand, operator, rightOperand } = condition; // Validate left operand - if (leftOperand.type === "question") { + if (leftOperand.type === "element") { const elementId = leftOperand.value; const elementInfo = allElements.get(elementId); @@ -3026,7 +3026,7 @@ const validateBlockConditions = ( if (element.type === TSurveyElementTypeEnum.OpenText) { // Validate right operand - if (rightOperand?.type === "question") { + if (rightOperand?.type === "element") { const elemId = rightOperand.value; const elem = allElements.get(elemId); @@ -3274,7 +3274,7 @@ const validateBlockActions = ( } if (variable.type === "text") { - if (action.value.type === "question") { + if (action.value.type === "element") { const allowedElements = [ TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.MultipleChoiceSingle, @@ -3297,7 +3297,7 @@ const validateBlockActions = ( return undefined; } - if (action.value.type === "question") { + if (action.value.type === "element") { const allowedElements = [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS]; const selectedElement = allElements.get(action.value.value); @@ -3980,7 +3980,7 @@ export type TSortOption = z.infer; export const ZSurveyRecallItem = z.object({ id: z.string(), label: z.string(), - type: z.enum(["question", "hiddenField", "attributeClass", "variable"]), + type: z.enum(["element", "hiddenField", "attributeClass", "variable"]), }); export type TSurveyRecallItem = z.infer;