mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 18:49:39 -06:00
fixes
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import { FormbricksAPI } from "@formbricks/api";
|
||||
import formbricks from "@formbricks/js/app";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
): {
|
||||
|
||||
@@ -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
|
||||
): {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user