Merge branch 'fix/blocks-migration' into fix/cta-logic-templates

This commit is contained in:
pandeymangg
2025-11-20 13:43:15 +05:30
25 changed files with 328 additions and 238 deletions

View File

@@ -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: {

View File

@@ -325,7 +325,7 @@ describe("getSurveySummaryDropOff", () => {
{
id: "c1",
leftOperand: {
type: "question" as const,
type: "element" as const,
value: "q2",
},
operator: "equals" as const,

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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" },
},

View File

@@ -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 },
};

View File

@@ -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") {

View File

@@ -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",

View File

@@ -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";
};

View File

@@ -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",
},
],

View File

@@ -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 },
},

View File

@@ -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 },
},

View File

@@ -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];

View File

@@ -75,7 +75,7 @@ export function ConditionalLogic({
id: createId(),
leftOperand: {
value: firstElement.id,
type: "question",
type: "element",
},
operator,
},

View File

@@ -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 };
}

View File

@@ -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" },
},

View File

@@ -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<TSingleCondition>): 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,

View File

@@ -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(

View File

@@ -96,7 +96,7 @@ export class RecallNode extends DecoratorNode<ReactNode> {
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;

View File

@@ -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<string, unknown> };
operator?: string;
leftOperand: { value: string; type: string; meta?: Record<string, unknown> };
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) {

View File

@@ -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" },
},
],

View File

@@ -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") {

View File

@@ -57,9 +57,9 @@ export const ZConnector = z.enum(["and", "or"]);
export type TConnector = z.infer<typeof ZConnector>;
// 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",
});

View File

@@ -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<typeof ZSortOption>;
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<typeof ZSurveyRecallItem>;