This commit is contained in:
pandeymangg
2024-09-20 09:51:35 +05:30
parent 4ed1747ee2
commit 350c895d8c
22 changed files with 360 additions and 335 deletions

View File

@@ -1,7 +1,6 @@
import { AdvancedLogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditorActions";
import { AdvancedLogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditorConditions";
import { ArrowRightIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
@@ -25,7 +24,7 @@ export function AdvancedLogicEditor({
isLast,
}: AdvancedLogicEditorProps) {
return (
<div className={cn("flex w-full grow flex-col gap-4 overflow-x-auto text-sm")}>
<div className="flex w-full grow flex-col gap-4 overflow-x-auto text-sm">
<AdvancedLogicEditorConditions
conditions={logicItem.conditions}
updateQuestion={updateQuestion}
@@ -42,12 +41,12 @@ export function AdvancedLogicEditor({
localSurvey={localSurvey}
questionIdx={questionIdx}
/>
{isLast && (
{isLast ? (
<div className="flex flex-wrap items-center space-x-2">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
</div>
)}
) : null}
</div>
);
}

View File

@@ -9,12 +9,12 @@ import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { getUpdatedActionBody } from "@formbricks/lib/survey/logic/utils";
import {
TAction,
TActionNumberVariableCalculateOperator,
TActionObjective,
TActionTextVariableCalculateOperator,
TActionVariableValueType,
TSurveyAdvancedLogic,
TSurveyAdvancedLogicAction,
} from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
@@ -47,21 +47,26 @@ export function AdvancedLogicEditorActions({
const handleActionsChange = (
operation: "remove" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: TAction
action?: TSurveyAdvancedLogicAction
) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
const actionsClone = logicItem.actions;
if (operation === "remove") {
actionsClone.splice(actionIdx, 1);
} else if (operation === "addBelow") {
actionsClone.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" });
} else if (operation === "duplicate") {
actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() });
} else if (operation === "update") {
if (!action) return;
actionsClone[actionIdx] = action;
switch (operation) {
case "remove":
actionsClone.splice(actionIdx, 1);
break;
case "addBelow":
actionsClone.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" });
break;
case "duplicate":
actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() });
break;
case "update":
if (!action) return;
actionsClone[actionIdx] = action;
break;
}
updateQuestion(questionIdx, {
@@ -75,9 +80,9 @@ export function AdvancedLogicEditorActions({
handleActionsChange("update", actionIdx, actionBody);
};
const handleValuesChange = (actionIdx: number, values: Partial<TAction>) => {
const handleValuesChange = (actionIdx: number, values: Partial<TSurveyAdvancedLogicAction>) => {
const action = actions[actionIdx];
const actionBody = { ...action, ...values } as TAction;
const actionBody = { ...action, ...values } as TSurveyAdvancedLogicAction;
handleActionsChange("update", actionIdx, actionBody);
};

View File

@@ -11,7 +11,7 @@ import {
addConditionBelow,
createGroupFromResource,
duplicateCondition,
isConditionsGroup,
isConditionGroup,
removeCondition,
toggleGroupConnector,
updateCondition,
@@ -32,7 +32,7 @@ import {
} from "@formbricks/ui/DropdownMenu";
import { InputCombobox, TComboboxOption } from "@formbricks/ui/InputCombobox";
interface AdvancedLogicEditorConditions {
interface AdvancedLogicEditorConditionsProps {
conditions: TConditionGroup;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
@@ -50,7 +50,7 @@ export function AdvancedLogicEditorConditions({
questionIdx,
updateQuestion,
depth = 0,
}: AdvancedLogicEditorConditions) {
}: AdvancedLogicEditorConditionsProps) {
const handleAddConditionBelow = (resourceId: string) => {
const operator = getDefaultOperatorForQuestion(question);
@@ -63,7 +63,7 @@ export function AdvancedLogicEditorConditions({
operator,
};
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
addConditionBelow(logicItem.conditions, resourceId, condition);
@@ -73,7 +73,7 @@ export function AdvancedLogicEditorConditions({
};
const handleConnectorChange = (groupId: string) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
toggleGroupConnector(logicItem.conditions, groupId);
@@ -83,7 +83,7 @@ export function AdvancedLogicEditorConditions({
};
const handleRemoveCondition = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
removeCondition(logicItem.conditions, resourceId);
@@ -98,7 +98,7 @@ export function AdvancedLogicEditorConditions({
};
const handleDuplicateCondition = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
duplicateCondition(logicItem.conditions, resourceId);
@@ -108,7 +108,7 @@ export function AdvancedLogicEditorConditions({
};
const handleCreateGroup = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
createGroupFromResource(logicItem.conditions, resourceId);
@@ -118,7 +118,7 @@ export function AdvancedLogicEditorConditions({
};
const handleUpdateCondition = (resourceId: string, updateConditionBody: Partial<TSingleCondition>) => {
const logicCopy = structuredClone(question.logic) || [];
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
updateCondition(logicItem.conditions, resourceId, updateConditionBody);
@@ -175,20 +175,21 @@ export function AdvancedLogicEditorConditions({
break;
}
};
const renderCondition = (
condition: TSingleCondition | TConditionGroup,
index: number,
parentConditionGroup: TConditionGroup
) => {
const connector = parentConditionGroup.connector;
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
return (
<div key={condition.id} className="flex items-start justify-between gap-4">
{index === 0 ? (
<div>When</div>
) : (
<div
className={cn("w-14", { "cursor-pointer underline": index === 1 })}
className={cn("w-14", index === 1 && "cursor-pointer underline")}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);
@@ -246,7 +247,7 @@ export function AdvancedLogicEditorConditions({
"When"
) : (
<div
className={cn("w-14", { "cursor-pointer underline": index === 1 })}
className={cn("w-14", index === 1 && "cursor-pointer underline")}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);

View File

@@ -19,16 +19,14 @@ export const AdvancedSettings = ({
attributeClasses,
}: AdvancedSettingsProps) => {
return (
<div>
<div className="mb-4">
<ConditionalLogic
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
</div>
<div className="flex flex-col gap-4">
<ConditionalLogic
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
<UpdateQuestionId
question={question}

View File

@@ -73,29 +73,31 @@ export function ConditionalLogic({
};
updateQuestion(questionIdx, {
logic: [...(question?.logic || []), initialCondition],
logic: [...(question?.logic ?? []), initialCondition],
});
};
const handleRemoveLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic || []);
const logicCopy = structuredClone(question.logic ?? []);
logicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const moveLogic = (from: number, to: number) => {
const logicCopy = structuredClone(question.logic || []);
const logicCopy = structuredClone(question.logic ?? []);
const [movedItem] = logicCopy.splice(from, 1);
logicCopy.splice(to, 0, movedItem);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const duplicateLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic || []);
const logicCopy = structuredClone(question.logic ?? []);
const logicItem = logicCopy[logicItemIdx];
const newLogicItem = duplicateLogicItem(logicItem);
logicCopy.splice(logicItemIdx + 1, 0, newLogicItem);
@@ -125,7 +127,7 @@ export function ConditionalLogic({
question={question}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
isLast={logicItemIdx === (question.logic || []).length - 1}
isLast={logicItemIdx === (question.logic ?? []).length - 1}
/>
<DropdownMenu>
@@ -152,7 +154,7 @@ export function ConditionalLogic({
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={logicItemIdx === (question.logic || []).length - 1}
disabled={logicItemIdx === (question.logic ?? []).length - 1}
onClick={() => {
moveLogic(logicItemIdx, logicItemIdx + 1);
}}>
@@ -183,7 +185,7 @@ export function ConditionalLogic({
size="sm"
variant="secondary"
EndIcon={PlusIcon}
onClick={() => addLogic()}>
onClick={addLogic}>
Add logic
</Button>
</div>

View File

@@ -39,24 +39,9 @@ export const PictureSelectionForm = ({
// Filter out the deleted choice from the choices array
const newChoices = question.choices?.filter((choice) => choice.id !== choiceValue) || [];
// // Update the logic, removing the deleted choice value
// const newLogic =
// question.logic?.map((logic) => {
// let updatedValue = logic.value;
// if (Array.isArray(logic.value)) {
// updatedValue = logic.value.filter((value) => value !== choiceValue);
// } else if (logic.value === choiceValue) {
// updatedValue = undefined;
// }
// return { ...logic, value: updatedValue };
// }) || [];
// Update the question with new choices and logic
updateQuestion(questionIdx, {
choices: newChoices,
// logic: newLogic
});
};

View File

@@ -18,17 +18,17 @@ import toast from "react-hot-toast";
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import { isConditionGroup } from "@formbricks/lib/survey/logic/utils";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import {
TAction,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogic,
TSurveyAdvancedLogicAction,
} from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation";
@@ -90,7 +90,7 @@ export const QuestionsView = ({
return {
...conditions,
conditions: conditions?.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
return updateConditions(condition);
} else {
return updateSingleCondition(condition);
@@ -106,14 +106,14 @@ export const QuestionsView = ({
updatedCondition.leftOperand = { ...condition.leftOperand, value: updatedId };
}
if (condition.rightOperand?.type === "question" && condition.rightOperand.value === compareId) {
if (condition.rightOperand?.type === "question" && condition.rightOperand?.value === compareId) {
updatedCondition.rightOperand = { ...condition.rightOperand, value: updatedId };
}
return updatedCondition;
};
const updateActions = (actions: TAction[]): TAction[] => {
const updateActions = (actions: TSurveyAdvancedLogicAction[]): TSurveyAdvancedLogicAction[] => {
return actions.map((action) => {
let updatedAction = { ...action };

View File

@@ -74,7 +74,7 @@ export const SurveyVariablesCardItem = ({
return () => subscription.unsubscribe();
}, [form, mode, editSurveyVariable]);
const onVaribleDelete = (variable: TSurveyVariable) => {
const onVariableDelete = (variable: TSurveyVariable) => {
const questions = [...localSurvey.questions];
const quesIdx = findVariableUsedInLogic(localSurvey, variable.id);
@@ -220,7 +220,7 @@ export const SurveyVariablesCardItem = ({
type="button"
size="sm"
className="whitespace-nowrap"
onClick={() => onVaribleDelete(variable)}>
onClick={() => onVariableDelete(variable)}>
<TrashIcon className="h-4 w-4" />
</Button>
)}

View File

@@ -1,89 +1,87 @@
import { ZSurveyLogicConditionsOperator } from "@formbricks/types/surveys/logic";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
export const ruleEngine = {
export const logicRules = {
question: {
[TSurveyQuestionTypeEnum.OpenText]: {
text: {
options: [
{
label: "equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
{
label: "is submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
},
number: {
options: [
{
label: "=",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicConditionsOperator.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
},
[`${TSurveyQuestionTypeEnum.OpenText}.text`]: {
options: [
{
label: "equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
{
label: "is submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
},
[`${TSurveyQuestionTypeEnum.OpenText}.number`]: {
options: [
{
label: "=",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicConditionsOperator.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: {
options: [
@@ -354,71 +352,69 @@ export const ruleEngine = {
],
},
},
variable: {
text: {
options: [
{
label: "equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
],
},
number: {
options: [
{
label: "=",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicConditionsOperator.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
],
},
["variable.text"]: {
options: [
{
label: "equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
],
},
["variable.number"]: {
options: [
{
label: "=",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicConditionsOperator.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicConditionsOperator.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
],
},
hiddenField: {
options: [
@@ -457,3 +453,5 @@ export const ruleEngine = {
],
},
};
export type TLogicRuleOption = (typeof logicRules.question)[keyof typeof logicRules.question]["options"];

View File

@@ -1,4 +1,3 @@
import { ruleEngine } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/logicRuleEngine";
import {
ArrowUpFromLineIcon,
CalendarDaysIcon,
@@ -20,14 +19,14 @@ import {
} from "lucide-react";
import { HTMLInputTypeAttribute } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import { isConditionGroup } from "@formbricks/lib/survey/logic/utils";
import {
TAction,
TConditionGroup,
TLeftOperand,
TRightOperand,
TSingleCondition,
TSurveyAdvancedLogic,
TSurveyAdvancedLogicAction,
TSurveyLogicConditionsOperator,
} from "@formbricks/types/surveys/logic";
import {
@@ -37,6 +36,7 @@ import {
TSurveyVariable,
} from "@formbricks/types/surveys/types";
import { TComboboxGroupedOption, TComboboxOption } from "@formbricks/ui/InputCombobox";
import { TLogicRuleOption, logicRules } from "./logicRuleEngine";
// formats the text to highlight specific parts of the text with slashes
export const formatTextWithSlashes = (text: string) => {
@@ -78,8 +78,8 @@ export const getConditionValueOptions = (
localSurvey: TSurvey,
currQuestionIdx: number
): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds || [];
const variables = localSurvey.variables || [];
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
const variables = localSurvey.variables ?? [];
const questions = localSurvey.questions;
const groupedOptions: TComboboxGroupedOption[] = [];
@@ -152,17 +152,17 @@ export const actionObjectiveOptions: TComboboxOption[] = [
];
const getQuestionOperatorOptions = (question: TSurveyQuestion): TComboboxOption[] => {
let options;
let options: TLogicRuleOption;
if (question.type === "openText") {
const inputType = question.inputType === "number" ? "number" : "text";
options = ruleEngine.question.openText[inputType].options;
options = logicRules.question[`openText.${inputType}`].options;
} else {
options = ruleEngine.question[question.type].options;
options = logicRules.question[question.type].options;
}
if (question.required) {
options = options.filter((option) => option.value !== "isSkipped");
options = options.filter((option) => option.value !== "isSkipped") as TLogicRuleOption;
}
return options;
@@ -179,14 +179,14 @@ export const getConditionOperatorOptions = (
localSurvey: TSurvey
): TComboboxOption[] => {
if (condition.leftOperand.type === "variable") {
const variables = localSurvey.variables || [];
const variables = localSurvey.variables ?? [];
const variableType =
variables.find((variable) => variable.id === condition.leftOperand.value)?.type || "text";
return ruleEngine.variable[variableType].options;
return logicRules[`variable.${variableType}`].options;
} else if (condition.leftOperand.type === "hiddenField") {
return ruleEngine.hiddenField.options;
return logicRules.hiddenField.options;
} else if (condition.leftOperand.type === "question") {
const questions = localSurvey.questions || [];
const questions = localSurvey.questions ?? [];
const question = questions.find((question) => question.id === condition.leftOperand.value);
if (!question) return [];
@@ -219,9 +219,9 @@ export const getMatchValueProps = (
return { show: false, options: [] };
}
let questions = localSurvey.questions || [];
let variables = localSurvey.variables || [];
let hiddenFields = localSurvey.hiddenFields?.fieldIds || [];
let questions = localSurvey.questions ?? [];
let variables = localSurvey.variables ?? [];
let hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
const selectedQuestion = questions.find((question) => question.id === condition.leftOperand.value);
const selectedVariable = variables.find((variable) => variable.id === condition.leftOperand.value);
@@ -763,7 +763,7 @@ export const getMatchValueProps = (
};
export const getActionTargetOptions = (
action: TAction,
action: TSurveyAdvancedLogicAction,
localSurvey: TSurvey,
currQuestionIdx: number
): TComboboxOption[] => {
@@ -797,7 +797,7 @@ export const getActionTargetOptions = (
};
export const getActionVariableOptions = (localSurvey: TSurvey): TComboboxOption[] => {
const variables = localSurvey.variables || [];
const variables = localSurvey.variables ?? [];
return variables.map((variable) => {
return {
@@ -851,8 +851,8 @@ export const getActionOpeartorOptions = (variableType?: TSurveyVariable["type"])
};
export const getActionValueOptions = (variableId: string, localSurvey: TSurvey): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds || [];
let variables = localSurvey.variables || [];
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
let variables = localSurvey.variables ?? [];
const questions = localSurvey.questions;
const hiddenFieldsOptions = hiddenFields.map((field) => {
@@ -995,29 +995,51 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
return [];
};
const isUsedInLeftOperand = (
leftOperand: TLeftOperand,
type: "question" | "hiddenField" | "variable",
id: string
): boolean => {
switch (type) {
case "question":
return leftOperand.type === "question" && leftOperand.value === id;
case "hiddenField":
return leftOperand.type === "hiddenField" && leftOperand.value === id;
case "variable":
return leftOperand.type === "variable" && leftOperand.value === id;
}
};
const isUsedInRightOperand = (
rightOperand: TRightOperand,
type: "question" | "hiddenField" | "variable",
id: string
): boolean => {
switch (type) {
case "question":
return rightOperand.type === "question" && rightOperand.value === id;
case "hiddenField":
return rightOperand.type === "hiddenField" && rightOperand.value === id;
case "variable":
return rightOperand.type === "variable" && rightOperand.value === id;
}
};
export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, questionId)) ||
isUsedInLeftOperand(condition.leftOperand, questionId)
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "question", questionId)) ||
isUsedInLeftOperand(condition.leftOperand, "question", questionId)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand, id: string): boolean => {
return leftOperand.type === "question" && leftOperand.value === id;
};
const isUsedInRightOperand = (rightOperand: TRightOperand, id: string): boolean => {
return rightOperand.type === "question" && rightOperand.value === id;
};
const isUsedInAction = (action: TAction): boolean => {
const isUsedInAction = (action: TSurveyAdvancedLogicAction): boolean => {
return (
(action.objective === "jumpToQuestion" && action.target === questionId) ||
(action.objective === "requireAnswer" && action.target === questionId)
@@ -1035,7 +1057,7 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): nu
export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optionId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
@@ -1066,27 +1088,19 @@ export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optio
export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand)) ||
isUsedInLeftOperand(condition.leftOperand)
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "variable", variableId)) ||
isUsedInLeftOperand(condition.leftOperand, "variable", variableId)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand): boolean => {
return leftOperand.type === "variable" && leftOperand.value === variableId;
};
const isUsedInRightOperand = (rightOperand: TRightOperand): boolean => {
return rightOperand.type === "variable" && rightOperand.value === variableId;
};
const isUsedInAction = (action: TAction): boolean => {
const isUsedInAction = (action: TSurveyAdvancedLogicAction): boolean => {
return action.objective === "calculate" && action.variableId === variableId;
};
@@ -1099,26 +1113,19 @@ export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): nu
export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: string): number => {
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
// It's a TConditionGroup
return condition.conditions.some(isUsedInCondition);
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand)) ||
isUsedInLeftOperand(condition.leftOperand)
(condition.rightOperand &&
isUsedInRightOperand(condition.rightOperand, "hiddenField", hiddenFieldId)) ||
isUsedInLeftOperand(condition.leftOperand, "hiddenField", hiddenFieldId)
);
}
};
const isUsedInLeftOperand = (leftOperand: TLeftOperand): boolean => {
return leftOperand.type === "hiddenField" && leftOperand.value === hiddenFieldId;
};
const isUsedInRightOperand = (rightOperand: TRightOperand): boolean => {
return rightOperand.type === "hiddenField" && rightOperand.value === hiddenFieldId;
};
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
return isUsedInCondition(logicRule.conditions);
};

View File

@@ -64,9 +64,12 @@ const mapResponsesToTableData = (responses: TResponse[], survey: TSurvey): TResp
responseId: response.id,
tags: response.tags,
notes: response.notes,
variables: survey.variables.reduce((acc, curr) => {
return Object.assign(acc, { [curr.id]: response.variables[curr.id] });
}, {}),
variables: survey.variables.reduce(
(acc, curr) => {
return Object.assign(acc, { [curr.id]: response.variables[curr.id] });
},
{} as Record<string, string | number>
),
verifiedEmail: typeof response.data["verifiedEmail"] === "string" ? response.data["verifiedEmail"] : "",
language: response.language,
person: response.person,

View File

@@ -48,6 +48,29 @@ export const TableSettingsModalItem = ({ column, survey }: TableSettingsModalIte
transform: CSS.Translate.toString(transform),
zIndex: isDragging ? 10 : 1,
};
const renderLabel = () => {
if (question) {
return (
<div className="flex items-center space-x-2">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[question.type]}</span>
<span className="max-w-xs truncate">{getLocalizedValue(question.headline, "default")}</span>
</div>
);
}
if (variable) {
return (
<div className="flex items-center space-x-2">
<span className="h-4 w-4">{VARIABLES_ICON_MAP[variable.type]}</span>
<span className="max-w-xs truncate">{variable.name}</span>
</div>
);
}
return <span className="max-w-xs truncate">{getLabelFromColumnId()}</span>;
};
return (
<div ref={setNodeRef} style={style} id={column.id}>
<div {...listeners} {...attributes}>
@@ -58,19 +81,7 @@ export const TableSettingsModalItem = ({ column, survey }: TableSettingsModalIte
<button onClick={(e) => e.preventDefault()}>
<GripVertical className="h-4 w-4" />
</button>
{question ? (
<div className="flex items-center space-x-2">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[question.type]}</span>
<span className="max-w-xs truncate">{getLocalizedValue(question.headline, "default")}</span>
</div>
) : variable ? (
<div className="flex items-center space-x-2">
<span className="h-4 w-4">{VARIABLES_ICON_MAP[variable.type]}</span>
<span className="max-w-xs truncate">{variable.name}</span>
</div>
) : (
<span className="max-w-xs truncate">{getLabelFromColumnId()}</span>
)}
{renderLabel()}
</div>
<Switch
id={column.id}

View File

@@ -1,4 +1,3 @@
// import { FormbricksAPI } from "@formbricks/api";
import formbricks from "@formbricks/js/app";
import { env } from "@formbricks/lib/env";

View File

@@ -4,10 +4,10 @@
import { createId } from "@paralleldrive/cuid2";
import { PrismaClient } from "@prisma/client";
import type {
TAction,
TRightOperand,
TSingleCondition,
TSurveyAdvancedLogic,
TSurveyAdvancedLogicAction,
TSurveyLogicConditionsOperator,
} from "@formbricks/types/surveys/logic";
import {
@@ -223,7 +223,7 @@ function convertLogic(
}
}
const action: TAction = {
const action: TSurveyAdvancedLogicAction = {
id: createId(),
objective: "jumpToQuestion",
target: actionTarget,

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { structuredClone } from "pollyfills/structuredClone";
import {
TResponse,
TResponseData,
@@ -620,10 +621,13 @@ export const getSurveySummaryDropOff = (
let impressionsArr = new Array(survey.questions.length).fill(0) as number[];
let dropOffPercentageArr = new Array(survey.questions.length).fill(0) as number[];
const surveyVariablesData = survey.variables?.reduce((acc, variable) => {
acc[variable.id] = variable.value;
return acc;
}, {});
const surveyVariablesData = survey.variables?.reduce(
(acc, variable) => {
acc[variable.id] = variable.value;
return acc;
},
{} as Record<string, string | number>
);
responses.forEach((response) => {
// Calculate total time-to-completion

View File

@@ -1,15 +1,15 @@
import { createId } from "@paralleldrive/cuid2";
import {
TAction,
TActionObjective,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogic,
TSurveyAdvancedLogicAction,
} from "@formbricks/types/surveys/logic";
type TCondition = TSingleCondition | TConditionGroup;
export const isConditionsGroup = (condition: TCondition): condition is TConditionGroup => {
export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => {
return (condition as TConditionGroup).connector !== undefined;
};
@@ -19,7 +19,7 @@ export const duplicateLogicItem = (logicItem: TSurveyAdvancedLogic): TSurveyAdva
...group,
id: createId(),
conditions: group.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
return duplicateConditionGroup(condition);
} else {
return duplicateCondition(condition);
@@ -35,7 +35,7 @@ export const duplicateLogicItem = (logicItem: TSurveyAdvancedLogic): TSurveyAdva
};
};
const duplicateAction = (action: TAction): TAction => {
const duplicateAction = (action: TSurveyAdvancedLogicAction): TSurveyAdvancedLogicAction => {
return {
...action,
id: createId(),
@@ -58,7 +58,7 @@ export const addConditionBelow = (
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (isConditionsGroup(item)) {
if (isConditionGroup(item)) {
if (item.id === resourceId) {
group.conditions.splice(i + 1, 0, condition);
break;
@@ -96,7 +96,7 @@ export const removeCondition = (group: TConditionGroup, resourceId: string) => {
return;
}
if (isConditionsGroup(item)) {
if (isConditionGroup(item)) {
removeCondition(item, resourceId);
}
}
@@ -127,9 +127,9 @@ export const deleteEmptyGroups = (group: TConditionGroup) => {
for (let i = 0; i < group.conditions.length; i++) {
const resource = group.conditions[i];
if (isConditionsGroup(resource) && resource.conditions.length === 0) {
if (isConditionGroup(resource) && resource.conditions.length === 0) {
group.conditions.splice(i, 1);
} else if (isConditionsGroup(resource)) {
} else if (isConditionGroup(resource)) {
deleteEmptyGroups(resource);
}
}
@@ -150,7 +150,7 @@ export const createGroupFromResource = (group: TConditionGroup, resourceId: stri
return;
}
if (isConditionsGroup(item)) {
if (isConditionGroup(item)) {
createGroupFromResource(item, resourceId);
}
}
@@ -169,13 +169,16 @@ export const updateCondition = (
return;
}
if (isConditionsGroup(item)) {
if (isConditionGroup(item)) {
updateCondition(item, resourceId, condition);
}
}
};
export const getUpdatedActionBody = (action: TAction, objective: TActionObjective): TAction => {
export const getUpdatedActionBody = (
action: TSurveyAdvancedLogicAction,
objective: TActionObjective
): TSurveyAdvancedLogicAction => {
if (objective === action.objective) return action;
switch (objective) {
case "calculate":

View File

@@ -1,9 +1,9 @@
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import {
TAction,
TActionCalculate,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogicAction,
} from "@formbricks/types/surveys/logic";
import {
TSurvey,
@@ -12,7 +12,7 @@ import {
TSurveyVariable,
} from "@formbricks/types/surveys/types";
import { getLocalizedValue } from "../i18n/utils";
import { isConditionsGroup } from "../survey/logic/utils";
import { isConditionGroup } from "../survey/logic/utils";
export const evaluateAdvancedLogic = (
localSurvey: TSurvey,
@@ -23,7 +23,7 @@ export const evaluateAdvancedLogic = (
): boolean => {
const evaluateConditionGroup = (group: TConditionGroup): boolean => {
const results = group.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
return evaluateConditionGroup(condition);
} else {
return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage);
@@ -291,20 +291,20 @@ const getLeftOperandValue = (
return choice.id;
} else if (Array.isArray(responseValue)) {
let choice: string[] = [];
let choices: string[] = [];
responseValue.forEach((value) => {
const foundChoice = currentQuestion.choices.find((choice) => {
return getLocalizedValue(choice.label, selectedLanguage) === value;
});
if (foundChoice) {
choice.push(foundChoice.id);
choices.push(foundChoice.id);
} else if (isOthersEnabled) {
choice.push("other");
choices.push("other");
}
});
if (choice) {
return Array.from(new Set(choice));
if (choices) {
return Array.from(new Set(choices));
}
}
}
@@ -359,7 +359,7 @@ const getRightOperandValue = (
export const performActions = (
survey: TSurvey,
actions: TAction[],
actions: TSurveyAdvancedLogicAction[],
data: TResponseData,
calculationResults: TResponseVariables
): {

View File

@@ -1,11 +1,11 @@
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionsGroup } from "@formbricks/lib/survey/logic/utils";
import { isConditionGroup } from "@formbricks/lib/survey/logic/utils";
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import {
TAction,
TActionCalculate,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogicAction,
} from "@formbricks/types/surveys/logic";
import {
TSurvey,
@@ -23,7 +23,7 @@ export const evaluateAdvancedLogic = (
): boolean => {
const evaluateConditionGroup = (group: TConditionGroup): boolean => {
const results = group.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
return evaluateConditionGroup(condition);
} else {
return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage);
@@ -359,7 +359,7 @@ const getRightOperandValue = (
export const performActions = (
survey: TSurvey,
actions: TAction[],
actions: TSurveyAdvancedLogicAction[],
data: TResponseData,
calculationResults: TResponseVariables
): {

View File

@@ -1,4 +1,4 @@
import { TAction, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurveyAdvancedLogic, TSurveyAdvancedLogicAction } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion, TSurveyQuestionChoice } from "@formbricks/types/surveys/types";
export const cn = (...classes: string[]) => {
@@ -64,7 +64,7 @@ const getPossibleNextQuestions = (question: TSurveyQuestion): string[] => {
const possibleDestinations: string[] = [];
question.logic.forEach((logic: TSurveyAdvancedLogic) => {
logic.actions.forEach((action: TAction) => {
logic.actions.forEach((action: TSurveyAdvancedLogicAction) => {
if (action.objective === "jumpToQuestion") {
possibleDestinations.push(action.target);
}

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { ZId } from "../common";
export const ZSurveyLogicConditionsOperator = z.enum([
"equals",
@@ -89,7 +90,7 @@ export type TRightOperand = z.infer<typeof ZRightOperand>;
export const ZSingleCondition = z
.object({
id: z.string().cuid2(),
id: ZId,
leftOperand: ZLeftOperand,
operator: ZSurveyLogicConditionsOperator,
rightOperand: ZRightOperand.optional(),
@@ -134,7 +135,7 @@ export interface TConditionGroup {
const ZConditionGroup: z.ZodType<TConditionGroup> = z.lazy(() =>
z.object({
id: z.string().cuid2(),
id: ZId,
connector: z.enum(["and", "or"]),
conditions: z.array(z.union([ZSingleCondition, ZConditionGroup])),
})
@@ -145,7 +146,7 @@ export const ZActionVariableValueType = z.union([z.literal("static"), ZDyanmicLo
export type TActionVariableValueType = z.infer<typeof ZActionVariableValueType>;
const ZActionBase = z.object({
id: z.string().cuid2(),
id: ZId,
objective: ZActionObjective,
});
@@ -205,14 +206,18 @@ const ZActionJumpToQuestion = ZActionBase.extend({
export type TActionJumpToQuestion = z.infer<typeof ZActionJumpToQuestion>;
export const ZAction = z.union([ZActionCalculate, ZActionRequireAnswer, ZActionJumpToQuestion]);
export const ZSurveyAdvancedLogicAction = z.union([
ZActionCalculate,
ZActionRequireAnswer,
ZActionJumpToQuestion,
]);
export type TAction = z.infer<typeof ZAction>;
export type TSurveyAdvancedLogicAction = z.infer<typeof ZSurveyAdvancedLogicAction>;
const ZSurveyAdvancedLogicActions = z.array(ZAction);
const ZSurveyAdvancedLogicActions = z.array(ZSurveyAdvancedLogicAction);
export const ZSurveyAdvancedLogic = z.object({
id: z.string().cuid2(),
id: ZId,
conditions: ZConditionGroup,
actions: ZSurveyAdvancedLogicActions,
});

View File

@@ -6,10 +6,10 @@ import { ZLanguage } from "../product";
import { ZSegment } from "../segment";
import { ZBaseStyling } from "../styling";
import {
type TAction,
type TConditionGroup,
type TSingleCondition,
type TSurveyAdvancedLogic,
type TSurveyAdvancedLogicAction,
type TSurveyLogicConditionsOperator,
ZActionCalculateNumber,
ZActionCalculateText,
@@ -19,7 +19,7 @@ import {
FORBIDDEN_IDS,
findLanguageCodesForDuplicateLabels,
findQuestionsWithCyclicLogic,
isConditionsGroup,
isConditionGroup,
validateCardFieldsForAllLanguages,
validateQuestionLabels,
} from "./validation";
@@ -1612,7 +1612,7 @@ const validateConditions = (
const validateConditionGroup = (group: TConditionGroup): void => {
group.conditions.forEach((condition) => {
if (isConditionsGroup(condition)) {
if (isConditionGroup(condition)) {
validateConditionGroup(condition);
} else {
validateSingleCondition(condition);
@@ -1629,7 +1629,7 @@ const validateActions = (
survey: TSurvey,
questionIndex: number,
logicIndex: number,
actions: TAction[]
actions: TSurveyAdvancedLogicAction[]
): z.ZodIssue[] => {
const questionIds = survey.questions.map((q) => q.id);

View File

@@ -1,5 +1,10 @@
import { z } from "zod";
import type { TAction, TActionJumpToQuestion, TConditionGroup, TSingleCondition } from "./logic";
import type {
TActionJumpToQuestion,
TConditionGroup,
TSingleCondition,
TSurveyAdvancedLogicAction,
} from "./logic";
import type { TI18nString, TSurveyLanguage, TSurveyQuestion } from "./types";
export const FORBIDDEN_IDS = [
@@ -225,7 +230,7 @@ export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestion[]): stri
};
// Helper function to find all "jumpToQuestion" actions in the logic
const findJumpToQuestionActions = (actions: TAction[]): TActionJumpToQuestion[] => {
const findJumpToQuestionActions = (actions: TSurveyAdvancedLogicAction[]): TActionJumpToQuestion[] => {
return actions.filter((action) => action.objective === "jumpToQuestion");
};
@@ -264,6 +269,6 @@ export const validateId = (
type TCondition = TSingleCondition | TConditionGroup;
export const isConditionsGroup = (condition: TCondition): condition is TConditionGroup => {
export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => {
return "conditions" in condition;
};