mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
fix: code review changes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { AdvancedLogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditor";
|
||||
import { getDefaultOperatorForQuestion } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
|
||||
import { getDefaultOperatorForQuestion } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
@@ -14,8 +14,7 @@ import { useMemo } from "react";
|
||||
import { duplicateLogicItem } from "@formbricks/lib/survey/logic/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -47,7 +46,7 @@ export function ConditionalLogic({
|
||||
const addLogic = () => {
|
||||
const operator = getDefaultOperatorForQuestion(question);
|
||||
|
||||
const initialCondition: TSurveyAdvancedLogic = {
|
||||
const initialCondition: TSurveyLogic = {
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
@@ -120,7 +119,7 @@ export function ConditionalLogic({
|
||||
<div
|
||||
key={logicItem.id}
|
||||
className="flex w-full grow items-start gap-2 rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<AdvancedLogicEditor
|
||||
<LogicEditor
|
||||
localSurvey={transformedSurvey}
|
||||
logicItem={logicItem}
|
||||
updateQuestion={updateQuestion}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
|
||||
import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
|
||||
import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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 { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions";
|
||||
import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface AdvancedLogicEditorProps {
|
||||
interface LogicEditorProps {
|
||||
localSurvey: TSurvey;
|
||||
logicItem: TSurveyAdvancedLogic;
|
||||
logicItem: TSurveyLogic;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
question: TSurveyQuestion;
|
||||
questionIdx: number;
|
||||
@@ -14,7 +13,7 @@ interface AdvancedLogicEditorProps {
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
export function AdvancedLogicEditor({
|
||||
export function LogicEditor({
|
||||
localSurvey,
|
||||
logicItem,
|
||||
updateQuestion,
|
||||
@@ -22,10 +21,10 @@ export function AdvancedLogicEditor({
|
||||
questionIdx,
|
||||
logicIdx,
|
||||
isLast,
|
||||
}: AdvancedLogicEditorProps) {
|
||||
}: LogicEditorProps) {
|
||||
return (
|
||||
<div className="flex w-full grow flex-col gap-4 overflow-x-auto text-sm">
|
||||
<AdvancedLogicEditorConditions
|
||||
<LogicEditorConditions
|
||||
conditions={logicItem.conditions}
|
||||
updateQuestion={updateQuestion}
|
||||
question={question}
|
||||
@@ -33,7 +32,7 @@ export function AdvancedLogicEditor({
|
||||
localSurvey={localSurvey}
|
||||
logicIdx={logicIdx}
|
||||
/>
|
||||
<AdvancedLogicEditorActions
|
||||
<LogicEditorActions
|
||||
logicItem={logicItem}
|
||||
logicIdx={logicIdx}
|
||||
question={question}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getActionTargetOptions,
|
||||
getActionValueOptions,
|
||||
getActionVariableOptions,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { getUpdatedActionBody } from "@formbricks/lib/survey/logic/utils";
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
TActionObjective,
|
||||
TActionTextVariableCalculateOperator,
|
||||
TActionVariableValueType,
|
||||
TSurveyAdvancedLogic,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -25,29 +26,29 @@ import {
|
||||
} from "@formbricks/ui/DropdownMenu";
|
||||
import { InputCombobox } from "@formbricks/ui/InputCombobox";
|
||||
|
||||
interface AdvancedLogicEditorActions {
|
||||
interface LogicEditorActions {
|
||||
localSurvey: TSurvey;
|
||||
logicItem: TSurveyAdvancedLogic;
|
||||
logicItem: TSurveyLogic;
|
||||
logicIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
questionIdx: number;
|
||||
}
|
||||
|
||||
export function AdvancedLogicEditorActions({
|
||||
export function LogicEditorActions({
|
||||
localSurvey,
|
||||
logicItem,
|
||||
logicIdx,
|
||||
question,
|
||||
updateQuestion,
|
||||
questionIdx,
|
||||
}: AdvancedLogicEditorActions) {
|
||||
}: LogicEditorActions) {
|
||||
const actions = logicItem.actions;
|
||||
|
||||
const handleActionsChange = (
|
||||
operation: "remove" | "addBelow" | "duplicate" | "update",
|
||||
actionIdx: number,
|
||||
action?: TSurveyAdvancedLogicAction
|
||||
action?: TSurveyLogicAction
|
||||
) => {
|
||||
const logicCopy = structuredClone(question.logic) ?? [];
|
||||
const logicItem = logicCopy[logicIdx];
|
||||
@@ -80,9 +81,9 @@ export function AdvancedLogicEditorActions({
|
||||
handleActionsChange("update", actionIdx, actionBody);
|
||||
};
|
||||
|
||||
const handleValuesChange = (actionIdx: number, values: Partial<TSurveyAdvancedLogicAction>) => {
|
||||
const handleValuesChange = (actionIdx: number, values: Partial<TSurveyLogicAction>) => {
|
||||
const action = actions[actionIdx];
|
||||
const actionBody = { ...action, ...values } as TSurveyAdvancedLogicAction;
|
||||
const actionBody = { ...action, ...values } as TSurveyLogicAction;
|
||||
handleActionsChange("update", actionIdx, actionBody);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
getConditionValueOptions,
|
||||
getDefaultOperatorForQuestion,
|
||||
getMatchValueProps,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { CopyIcon, MoreVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -21,9 +21,10 @@ import {
|
||||
TDyanmicLogicField,
|
||||
TRightOperand,
|
||||
TSingleCondition,
|
||||
TSurvey,
|
||||
TSurveyLogicConditionsOperator,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
TSurveyQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
} from "@formbricks/ui/DropdownMenu";
|
||||
import { InputCombobox, TComboboxOption } from "@formbricks/ui/InputCombobox";
|
||||
|
||||
interface AdvancedLogicEditorConditionsProps {
|
||||
interface LogicEditorConditionsProps {
|
||||
conditions: TConditionGroup;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
question: TSurveyQuestion;
|
||||
@@ -42,7 +43,7 @@ interface AdvancedLogicEditorConditionsProps {
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export function AdvancedLogicEditorConditions({
|
||||
export function LogicEditorConditions({
|
||||
conditions,
|
||||
logicIdx,
|
||||
question,
|
||||
@@ -50,7 +51,7 @@ export function AdvancedLogicEditorConditions({
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
depth = 0,
|
||||
}: AdvancedLogicEditorConditionsProps) {
|
||||
}: LogicEditorConditionsProps) {
|
||||
const handleAddConditionBelow = (resourceId: string) => {
|
||||
const operator = getDefaultOperatorForQuestion(question);
|
||||
|
||||
@@ -198,7 +199,7 @@ export function AdvancedLogicEditorConditions({
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-lg border border-slate-400 p-3">
|
||||
<AdvancedLogicEditorConditions
|
||||
<LogicEditorConditions
|
||||
conditions={condition}
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RankingQuestionForm";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { AddEndingCardButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddEndingCardButton";
|
||||
import { SurveyVariablesCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyVariablesCard";
|
||||
import { findQuestionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { findQuestionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@@ -27,9 +27,9 @@ import { TProduct } from "@formbricks/types/product";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogic,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation";
|
||||
import {
|
||||
@@ -115,7 +115,7 @@ export const QuestionsView = ({
|
||||
return updatedCondition;
|
||||
};
|
||||
|
||||
const updateActions = (actions: TSurveyAdvancedLogicAction[]): TSurveyAdvancedLogicAction[] => {
|
||||
const updateActions = (actions: TSurveyLogicAction[]): TSurveyLogicAction[] => {
|
||||
return actions.map((action) => {
|
||||
let updatedAction = { ...action };
|
||||
|
||||
@@ -145,7 +145,7 @@ export const QuestionsView = ({
|
||||
|
||||
// Update advanced logic
|
||||
if (question.logic) {
|
||||
updatedQuestion.logic = question.logic.map((logicRule: TSurveyAdvancedLogic) => ({
|
||||
updatedQuestion.logic = question.logic.map((logicRule: TSurveyLogic) => ({
|
||||
...logicRule,
|
||||
conditions: updateConditions(logicRule.conditions),
|
||||
actions: updateActions(logicRule.actions),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { findVariableUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { findVariableUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ZSurveyLogicConditionsOperator } from "@formbricks/types/surveys/logic";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyQuestionTypeEnum, ZSurveyLogicConditionsOperator } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const logicRules = {
|
||||
question: {
|
||||
|
||||
@@ -1,36 +1,17 @@
|
||||
import {
|
||||
ArrowUpFromLineIcon,
|
||||
CalendarDaysIcon,
|
||||
CheckIcon,
|
||||
EyeOffIcon,
|
||||
FileDigitIcon,
|
||||
FileType2Icon,
|
||||
Grid3X3Icon,
|
||||
HomeIcon,
|
||||
ImageIcon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
MessageSquareTextIcon,
|
||||
MousePointerClickIcon,
|
||||
PhoneIcon,
|
||||
PresentationIcon,
|
||||
Rows3Icon,
|
||||
StarIcon,
|
||||
} from "lucide-react";
|
||||
import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react";
|
||||
import { HTMLInputTypeAttribute } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { isConditionGroup } from "@formbricks/lib/survey/logic/utils";
|
||||
import { questionTypes } from "@formbricks/lib/utils/questions";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TLeftOperand,
|
||||
TRightOperand,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogic,
|
||||
TSurveyAdvancedLogicAction,
|
||||
TSurveyLogicConditionsOperator,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyLogicConditionsOperator,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
@@ -57,22 +38,13 @@ export const formatTextWithSlashes = (text: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const questionIconMapping = {
|
||||
openText: MessageSquareTextIcon,
|
||||
multipleChoiceSingle: Rows3Icon,
|
||||
multipleChoiceMulti: ListIcon,
|
||||
pictureSelection: ImageIcon,
|
||||
rating: StarIcon,
|
||||
nps: PresentationIcon,
|
||||
cta: MousePointerClickIcon,
|
||||
consent: CheckIcon,
|
||||
date: CalendarDaysIcon,
|
||||
fileUpload: ArrowUpFromLineIcon,
|
||||
cal: PhoneIcon,
|
||||
matrix: Grid3X3Icon,
|
||||
ranking: ListOrderedIcon,
|
||||
address: HomeIcon,
|
||||
};
|
||||
const questionIconMapping = questionTypes.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr.id]: curr.icon,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export const getConditionValueOptions = (
|
||||
localSurvey: TSurvey,
|
||||
@@ -763,7 +735,7 @@ export const getMatchValueProps = (
|
||||
};
|
||||
|
||||
export const getActionTargetOptions = (
|
||||
action: TSurveyAdvancedLogicAction,
|
||||
action: TSurveyLogicAction,
|
||||
localSurvey: TSurvey,
|
||||
currQuestionIdx: number
|
||||
): TComboboxOption[] => {
|
||||
@@ -1039,14 +1011,14 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): nu
|
||||
}
|
||||
};
|
||||
|
||||
const isUsedInAction = (action: TSurveyAdvancedLogicAction): boolean => {
|
||||
const isUsedInAction = (action: TSurveyLogicAction): boolean => {
|
||||
return (
|
||||
(action.objective === "jumpToQuestion" && action.target === questionId) ||
|
||||
(action.objective === "requireAnswer" && action.target === questionId)
|
||||
);
|
||||
};
|
||||
|
||||
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
|
||||
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
|
||||
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
|
||||
};
|
||||
|
||||
@@ -1079,7 +1051,7 @@ export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optio
|
||||
return false;
|
||||
};
|
||||
|
||||
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
|
||||
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
|
||||
return isUsedInCondition(logicRule.conditions);
|
||||
};
|
||||
|
||||
@@ -1100,11 +1072,11 @@ export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): nu
|
||||
}
|
||||
};
|
||||
|
||||
const isUsedInAction = (action: TSurveyAdvancedLogicAction): boolean => {
|
||||
const isUsedInAction = (action: TSurveyLogicAction): boolean => {
|
||||
return action.objective === "calculate" && action.variableId === variableId;
|
||||
};
|
||||
|
||||
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
|
||||
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
|
||||
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
|
||||
};
|
||||
|
||||
@@ -1126,7 +1098,7 @@ export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: strin
|
||||
}
|
||||
};
|
||||
|
||||
const isUsedInLogicRule = (logicRule: TSurveyAdvancedLogic): boolean => {
|
||||
const isUsedInLogicRule = (logicRule: TSurveyLogic): boolean => {
|
||||
return isUsedInCondition(logicRule.conditions);
|
||||
};
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
/* eslint-disable no-console -- logging is allowed in migration scripts */
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import type {
|
||||
TRightOperand,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogic,
|
||||
TSurveyAdvancedLogicAction,
|
||||
TSurveyLogicConditionsOperator,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
type TRightOperand,
|
||||
type TSingleCondition,
|
||||
type TSurveyEndings,
|
||||
type TSurveyLogic,
|
||||
type TSurveyLogicAction,
|
||||
type TSurveyLogicConditionsOperator,
|
||||
type TSurveyMultipleChoiceQuestion,
|
||||
type TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
@@ -25,7 +23,7 @@ interface TOldLogic {
|
||||
destination: string;
|
||||
}
|
||||
|
||||
const isOldLogic = (logic: TOldLogic | TSurveyAdvancedLogic): logic is TOldLogic => {
|
||||
const isOldLogic = (logic: TOldLogic | TSurveyLogic): logic is TOldLogic => {
|
||||
return Object.keys(logic).some((key) => ["condition", "destination", "value"].includes(key));
|
||||
};
|
||||
|
||||
@@ -202,7 +200,7 @@ function convertLogic(
|
||||
surveyEndings: TSurveyEndings,
|
||||
oldLogic: TOldLogic,
|
||||
question: TSurveyQuestion
|
||||
): TSurveyAdvancedLogic | undefined {
|
||||
): TSurveyLogic | undefined {
|
||||
if (!oldLogic.condition || !oldLogic.destination) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -223,7 +221,7 @@ function convertLogic(
|
||||
}
|
||||
}
|
||||
|
||||
const action: TSurveyAdvancedLogicAction = {
|
||||
const action: TSurveyLogicAction = {
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: actionTarget,
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import { getLocalizedValue } from "../i18n/utils";
|
||||
import { processResponseData } from "../responses";
|
||||
import { getTodaysDateTimeFormatted } from "../time";
|
||||
import { evaluateAdvancedLogic, performActions } from "../utils/evaluateLogic";
|
||||
import { evaluateLogic, performActions } from "../utils/evaluateLogic";
|
||||
import { sanitizeString } from "../utils/strings";
|
||||
|
||||
export const calculateTtcTotal = (ttc: TResponseTtc) => {
|
||||
@@ -638,7 +638,7 @@ export const getSurveySummaryDropOff = (
|
||||
}
|
||||
});
|
||||
|
||||
let localSurvey = JSON.parse(JSON.stringify(survey)) as TSurvey;
|
||||
let localSurvey = structuredClone(survey);
|
||||
let localResponseData: TResponseData = { ...response.data };
|
||||
let localVariables: TResponseVariables = {
|
||||
...surveyVariablesData,
|
||||
@@ -742,15 +742,7 @@ const evaluateLogicAndGetNextQuestionId = (
|
||||
|
||||
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
|
||||
for (const logic of currQuesTemp.logic) {
|
||||
if (
|
||||
evaluateAdvancedLogic(
|
||||
localSurvey,
|
||||
data,
|
||||
localVariables,
|
||||
logic.conditions,
|
||||
selectedLanguage ?? "default"
|
||||
)
|
||||
) {
|
||||
if (evaluateLogic(localSurvey, data, localVariables, logic.conditions, selectedLanguage ?? "default")) {
|
||||
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
|
||||
updatedSurvey,
|
||||
logic.actions,
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
TActionObjective,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogic,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
type TCondition = TSingleCondition | TConditionGroup;
|
||||
|
||||
@@ -13,7 +13,7 @@ export const isConditionGroup = (condition: TCondition): condition is TCondition
|
||||
return (condition as TConditionGroup).connector !== undefined;
|
||||
};
|
||||
|
||||
export const duplicateLogicItem = (logicItem: TSurveyAdvancedLogic): TSurveyAdvancedLogic => {
|
||||
export const duplicateLogicItem = (logicItem: TSurveyLogic): TSurveyLogic => {
|
||||
const duplicateConditionGroup = (group: TConditionGroup): TConditionGroup => {
|
||||
return {
|
||||
...group,
|
||||
@@ -35,7 +35,7 @@ export const duplicateLogicItem = (logicItem: TSurveyAdvancedLogic): TSurveyAdva
|
||||
};
|
||||
};
|
||||
|
||||
const duplicateAction = (action: TSurveyAdvancedLogicAction): TSurveyAdvancedLogicAction => {
|
||||
const duplicateAction = (action: TSurveyLogicAction): TSurveyLogicAction => {
|
||||
return {
|
||||
...action,
|
||||
id: createId(),
|
||||
@@ -176,9 +176,9 @@ export const updateCondition = (
|
||||
};
|
||||
|
||||
export const getUpdatedActionBody = (
|
||||
action: TSurveyAdvancedLogicAction,
|
||||
action: TSurveyLogicAction,
|
||||
objective: TActionObjective
|
||||
): TSurveyAdvancedLogicAction => {
|
||||
): TSurveyLogicAction => {
|
||||
if (objective === action.objective) return action;
|
||||
switch (objective) {
|
||||
case "calculate":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from "../../__mocks__/database";
|
||||
import { mockResponseNote, mockResponseWithMockPerson } from "../../response/tests/__mocks__/data.mock";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { evaluateAdvancedLogic } from "utils/evaluateLogic";
|
||||
import { evaluateLogic } from "utils/evaluateLogic";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { testInputValidation } from "vitestSetup";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
@@ -39,12 +39,12 @@ beforeEach(() => {
|
||||
prisma.survey.count.mockResolvedValue(1);
|
||||
});
|
||||
|
||||
describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
describe("evaluateLogic with mockSurveyWithLogic", () => {
|
||||
it("should return true when q1 answer is blue", () => {
|
||||
const data = { q1: "blue" };
|
||||
const variablesData = {};
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -58,7 +58,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q1: "red" };
|
||||
const variablesData = {};
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -72,7 +72,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q1: "blue", q2: "pizza" };
|
||||
const variablesData = {};
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -86,7 +86,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q1: "blue", q2: "burger" };
|
||||
const variablesData = {};
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -100,7 +100,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q2: "pizza", q3: "Inception" };
|
||||
const variablesData = {};
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -114,7 +114,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q4: "lmao" };
|
||||
const variablesData = { siog1dabtpo3l0a3xoxw2922: "lmao" };
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -128,7 +128,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q4: "lol" };
|
||||
const variablesData = { siog1dabtpo3l0a3xoxw2922: "damn" };
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -142,7 +142,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q5: "40" };
|
||||
const variablesData = { km1srr55owtn2r7lkoh5ny1u: 35 };
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -156,7 +156,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q5: "40" };
|
||||
const variablesData = { km1srr55owtn2r7lkoh5ny1u: 25 };
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
@@ -170,7 +170,7 @@ describe("evaluateAdvancedLogic with mockSurveyWithLogic", () => {
|
||||
const data = { q6: ["lmao", "XD"], q1: "green", q2: "pizza", q3: "inspection", name: "pizza" };
|
||||
const variablesData = { siog1dabtpo3l0a3xoxw2922: "tokyo" };
|
||||
|
||||
const result = evaluateAdvancedLogic(
|
||||
const result = evaluateLogic(
|
||||
mockSurveyWithLogic,
|
||||
data,
|
||||
variablesData,
|
||||
|
||||
@@ -3,10 +3,8 @@ import {
|
||||
TActionCalculate,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
@@ -14,7 +12,7 @@ import {
|
||||
import { getLocalizedValue } from "../i18n/utils";
|
||||
import { isConditionGroup } from "../survey/logic/utils";
|
||||
|
||||
export const evaluateAdvancedLogic = (
|
||||
export const evaluateLogic = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
@@ -359,7 +357,7 @@ const getRightOperandValue = (
|
||||
|
||||
export const performActions = (
|
||||
survey: TSurvey,
|
||||
actions: TSurveyAdvancedLogicAction[],
|
||||
actions: TSurveyLogicAction[],
|
||||
data: TResponseData,
|
||||
calculationResults: TResponseVariables
|
||||
): {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SurveyCloseButton } from "@/components/general/SurveyCloseButton";
|
||||
import { WelcomeCard } from "@/components/general/WelcomeCard";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/StackedCardsContainer";
|
||||
import { evaluateAdvancedLogic, performActions } from "@/lib/logicEvaluator";
|
||||
import { evaluateLogic, performActions } from "@/lib/logicEvaluator";
|
||||
import { parseRecallInformation } from "@/lib/recall";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
@@ -179,14 +179,18 @@ export const Survey = ({
|
||||
};
|
||||
|
||||
const makeQuestionsRequired = (questionIds: string[]): void => {
|
||||
const localSurveyClone = structuredClone(localSurvey);
|
||||
localSurveyClone.questions.forEach((question) => {
|
||||
if (questionIds.includes(question.id)) {
|
||||
question.required = true;
|
||||
}
|
||||
});
|
||||
|
||||
setlocalSurvey(localSurveyClone);
|
||||
setlocalSurvey((prevSurvey) => ({
|
||||
...prevSurvey,
|
||||
questions: prevSurvey.questions.map((question) => {
|
||||
if (questionIds.includes(question.id)) {
|
||||
return {
|
||||
...question,
|
||||
required: true,
|
||||
};
|
||||
}
|
||||
return question;
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
const pushVariableState = (questionId: string) => {
|
||||
@@ -224,13 +228,7 @@ export const Survey = ({
|
||||
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
|
||||
for (const logic of currQuesTemp.logic) {
|
||||
if (
|
||||
evaluateAdvancedLogic(
|
||||
localSurvey,
|
||||
localResponseData,
|
||||
currentVariables,
|
||||
logic.conditions,
|
||||
selectedLanguage
|
||||
)
|
||||
evaluateLogic(localSurvey, localResponseData, currentVariables, logic.conditions, selectedLanguage)
|
||||
) {
|
||||
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
|
||||
localSurvey,
|
||||
|
||||
@@ -5,16 +5,14 @@ import {
|
||||
TActionCalculate,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "@formbricks/types/surveys/logic";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
export const evaluateAdvancedLogic = (
|
||||
export const evaluateLogic = (
|
||||
localSurvey: TSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
@@ -359,7 +357,7 @@ const getRightOperandValue = (
|
||||
|
||||
export const performActions = (
|
||||
survey: TSurvey,
|
||||
actions: TSurveyAdvancedLogicAction[],
|
||||
actions: TSurveyLogicAction[],
|
||||
data: TResponseData,
|
||||
calculationResults: TResponseVariables
|
||||
): {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { TSurveyAdvancedLogic, TSurveyAdvancedLogicAction } from "@formbricks/types/surveys/logic";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionChoice } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
export const cn = (...classes: string[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
@@ -63,8 +68,8 @@ const getPossibleNextQuestions = (question: TSurveyQuestion): string[] => {
|
||||
|
||||
const possibleDestinations: string[] = [];
|
||||
|
||||
question.logic.forEach((logic: TSurveyAdvancedLogic) => {
|
||||
logic.actions.forEach((action: TSurveyAdvancedLogicAction) => {
|
||||
question.logic.forEach((logic: TSurveyLogic) => {
|
||||
logic.actions.forEach((action: TSurveyLogicAction) => {
|
||||
if (action.objective === "jumpToQuestion") {
|
||||
possibleDestinations.push(action.target);
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ZId } from "../common";
|
||||
|
||||
export const ZSurveyLogicConditionsOperator = z.enum([
|
||||
"equals",
|
||||
"doesNotEqual",
|
||||
"contains",
|
||||
"doesNotContain",
|
||||
"startsWith",
|
||||
"doesNotStartWith",
|
||||
"endsWith",
|
||||
"doesNotEndWith",
|
||||
"isSubmitted",
|
||||
"isSkipped",
|
||||
"isGreaterThan",
|
||||
"isLessThan",
|
||||
"isGreaterThanOrEqual",
|
||||
"isLessThanOrEqual",
|
||||
"equalsOneOf",
|
||||
"includesAllOf",
|
||||
"includesOneOf",
|
||||
"isClicked",
|
||||
"isAccepted",
|
||||
"isBefore",
|
||||
"isAfter",
|
||||
"isBooked",
|
||||
"isPartiallySubmitted",
|
||||
"isCompletelySubmitted",
|
||||
]);
|
||||
|
||||
const operatorsWithoutRightOperand = [
|
||||
ZSurveyLogicConditionsOperator.Enum.isSubmitted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isSkipped,
|
||||
ZSurveyLogicConditionsOperator.Enum.isClicked,
|
||||
ZSurveyLogicConditionsOperator.Enum.isAccepted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isBooked,
|
||||
ZSurveyLogicConditionsOperator.Enum.isPartiallySubmitted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isCompletelySubmitted,
|
||||
] as const;
|
||||
|
||||
export const ZDyanmicLogicField = z.enum(["question", "variable", "hiddenField"]);
|
||||
export const ZActionObjective = z.enum(["calculate", "requireAnswer", "jumpToQuestion"]);
|
||||
export const ZActionTextVariableCalculateOperator = z.enum(["assign", "concat"], {
|
||||
message: "Conditional Logic: Invalid operator for a text variable",
|
||||
});
|
||||
export const ZActionNumberVariableCalculateOperator = z.enum(
|
||||
["add", "subtract", "multiply", "divide", "assign"],
|
||||
{ message: "Conditional Logic: Invalid operator for a number variable" }
|
||||
);
|
||||
|
||||
const ZDynamicQuestion = z.object({
|
||||
type: z.literal("question"),
|
||||
value: z.string().min(1, "Conditional Logic: Question id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicVariable = z.object({
|
||||
type: z.literal("variable"),
|
||||
value: z
|
||||
.string()
|
||||
.cuid2({ message: "Conditional Logic: Variable id must be a valid cuid" })
|
||||
.min(1, "Conditional Logic: Variable id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicHiddenField = z.object({
|
||||
type: z.literal("hiddenField"),
|
||||
value: z.string().min(1, "Conditional Logic: Hidden field id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicLogicFieldValue = z.union([ZDynamicQuestion, ZDynamicVariable, ZDynamicHiddenField], {
|
||||
message: "Conditional Logic: Invalid dynamic field value",
|
||||
});
|
||||
|
||||
export type TSurveyLogicConditionsOperator = z.infer<typeof ZSurveyLogicConditionsOperator>;
|
||||
export type TDyanmicLogicField = z.infer<typeof ZDyanmicLogicField>;
|
||||
export type TActionObjective = z.infer<typeof ZActionObjective>;
|
||||
export type TActionTextVariableCalculateOperator = z.infer<typeof ZActionTextVariableCalculateOperator>;
|
||||
export type TActionNumberVariableCalculateOperator = z.infer<typeof ZActionNumberVariableCalculateOperator>;
|
||||
|
||||
// Conditions
|
||||
const ZLeftOperand = ZDynamicLogicFieldValue;
|
||||
export type TLeftOperand = z.infer<typeof ZLeftOperand>;
|
||||
|
||||
export const ZRightOperandStatic = z.object({
|
||||
type: z.literal("static"),
|
||||
value: z.union([z.string(), z.number(), z.array(z.string())]),
|
||||
});
|
||||
|
||||
export const ZRightOperand = z.union([ZRightOperandStatic, ZDynamicLogicFieldValue]);
|
||||
export type TRightOperand = z.infer<typeof ZRightOperand>;
|
||||
|
||||
export const ZSingleCondition = z
|
||||
.object({
|
||||
id: ZId,
|
||||
leftOperand: ZLeftOperand,
|
||||
operator: ZSurveyLogicConditionsOperator,
|
||||
rightOperand: ZRightOperand.optional(),
|
||||
})
|
||||
.and(
|
||||
z.object({
|
||||
connector: z.undefined(),
|
||||
})
|
||||
)
|
||||
.superRefine((val, ctx) => {
|
||||
if (
|
||||
!operatorsWithoutRightOperand.includes(val.operator as (typeof operatorsWithoutRightOperand)[number])
|
||||
) {
|
||||
if (val.rightOperand === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand is required for operator "${val.operator}"`,
|
||||
path: ["rightOperand"],
|
||||
});
|
||||
} else if (val.rightOperand.type === "static" && val.rightOperand.value === "") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand value cannot be empty for operator "${val.operator}"`,
|
||||
});
|
||||
}
|
||||
} else if (val.rightOperand !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand should not be present for operator "${val.operator}"`,
|
||||
path: ["rightOperand"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type TSingleCondition = z.infer<typeof ZSingleCondition>;
|
||||
|
||||
export interface TConditionGroup {
|
||||
id: string;
|
||||
connector: "and" | "or";
|
||||
conditions: (TSingleCondition | TConditionGroup)[];
|
||||
}
|
||||
|
||||
const ZConditionGroup: z.ZodType<TConditionGroup> = z.lazy(() =>
|
||||
z.object({
|
||||
id: ZId,
|
||||
connector: z.enum(["and", "or"]),
|
||||
conditions: z.array(z.union([ZSingleCondition, ZConditionGroup])),
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
export const ZActionVariableValueType = z.union([z.literal("static"), ZDyanmicLogicField]);
|
||||
export type TActionVariableValueType = z.infer<typeof ZActionVariableValueType>;
|
||||
|
||||
const ZActionBase = z.object({
|
||||
id: ZId,
|
||||
objective: ZActionObjective,
|
||||
});
|
||||
|
||||
export type TActionBase = z.infer<typeof ZActionBase>;
|
||||
|
||||
const ZActionCalculateBase = ZActionBase.extend({
|
||||
objective: z.literal("calculate"),
|
||||
variableId: z.string(),
|
||||
});
|
||||
|
||||
export const ZActionCalculateText = ZActionCalculateBase.extend({
|
||||
operator: ZActionTextVariableCalculateOperator,
|
||||
value: z.union([
|
||||
z.object({
|
||||
type: z.literal("static"),
|
||||
value: z
|
||||
.string({ message: "Conditional Logic: Value must be a string for text variable" })
|
||||
.min(1, "Conditional Logic: Please enter a value in logic field"),
|
||||
}),
|
||||
ZDynamicLogicFieldValue,
|
||||
]),
|
||||
});
|
||||
|
||||
export const ZActionCalculateNumber = ZActionCalculateBase.extend({
|
||||
operator: ZActionNumberVariableCalculateOperator,
|
||||
value: z.union([
|
||||
z.object({
|
||||
type: z.literal("static"),
|
||||
value: z.number({ message: "Conditional Logic: Value must be a number for number variable" }),
|
||||
}),
|
||||
ZDynamicLogicFieldValue,
|
||||
]),
|
||||
}).superRefine((val, ctx) => {
|
||||
if (val.operator === "divide" && val.value.type === "static" && val.value.value === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Conditional Logic: Cannot divide by zero",
|
||||
path: ["value", "value"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const ZActionCalculate = z.union([ZActionCalculateText, ZActionCalculateNumber]);
|
||||
|
||||
export type TActionCalculate = z.infer<typeof ZActionCalculate>;
|
||||
|
||||
const ZActionRequireAnswer = ZActionBase.extend({
|
||||
objective: z.literal("requireAnswer"),
|
||||
target: z.string().min(1, "Conditional Logic: Target question id cannot be empty"),
|
||||
});
|
||||
export type TActionRequireAnswer = z.infer<typeof ZActionRequireAnswer>;
|
||||
|
||||
const ZActionJumpToQuestion = ZActionBase.extend({
|
||||
objective: z.literal("jumpToQuestion"),
|
||||
target: z.string().min(1, "Conditional Logic: Target question id cannot be empty"),
|
||||
});
|
||||
|
||||
export type TActionJumpToQuestion = z.infer<typeof ZActionJumpToQuestion>;
|
||||
|
||||
export const ZSurveyAdvancedLogicAction = z.union([
|
||||
ZActionCalculate,
|
||||
ZActionRequireAnswer,
|
||||
ZActionJumpToQuestion,
|
||||
]);
|
||||
|
||||
export type TSurveyAdvancedLogicAction = z.infer<typeof ZSurveyAdvancedLogicAction>;
|
||||
|
||||
const ZSurveyAdvancedLogicActions = z.array(ZSurveyAdvancedLogicAction);
|
||||
|
||||
export const ZSurveyAdvancedLogic = z.object({
|
||||
id: ZId,
|
||||
conditions: ZConditionGroup,
|
||||
actions: ZSurveyAdvancedLogicActions,
|
||||
});
|
||||
|
||||
export type TSurveyAdvancedLogic = z.infer<typeof ZSurveyAdvancedLogic>;
|
||||
@@ -5,16 +5,6 @@ import { ZAllowedFileExtension, ZColor, ZId, ZPlacement } from "../common";
|
||||
import { ZLanguage } from "../product";
|
||||
import { ZSegment } from "../segment";
|
||||
import { ZBaseStyling } from "../styling";
|
||||
import {
|
||||
type TConditionGroup,
|
||||
type TSingleCondition,
|
||||
type TSurveyAdvancedLogic,
|
||||
type TSurveyAdvancedLogicAction,
|
||||
type TSurveyLogicConditionsOperator,
|
||||
ZActionCalculateNumber,
|
||||
ZActionCalculateText,
|
||||
ZSurveyAdvancedLogic,
|
||||
} from "./logic";
|
||||
import {
|
||||
FORBIDDEN_IDS,
|
||||
findLanguageCodesForDuplicateLabels,
|
||||
@@ -238,6 +228,226 @@ export const ZSurveyPictureChoice = z.object({
|
||||
|
||||
export type TSurveyQuestionChoice = z.infer<typeof ZSurveyQuestionChoice>;
|
||||
|
||||
// Logic types
|
||||
export const ZSurveyLogicConditionsOperator = z.enum([
|
||||
"equals",
|
||||
"doesNotEqual",
|
||||
"contains",
|
||||
"doesNotContain",
|
||||
"startsWith",
|
||||
"doesNotStartWith",
|
||||
"endsWith",
|
||||
"doesNotEndWith",
|
||||
"isSubmitted",
|
||||
"isSkipped",
|
||||
"isGreaterThan",
|
||||
"isLessThan",
|
||||
"isGreaterThanOrEqual",
|
||||
"isLessThanOrEqual",
|
||||
"equalsOneOf",
|
||||
"includesAllOf",
|
||||
"includesOneOf",
|
||||
"isClicked",
|
||||
"isAccepted",
|
||||
"isBefore",
|
||||
"isAfter",
|
||||
"isBooked",
|
||||
"isPartiallySubmitted",
|
||||
"isCompletelySubmitted",
|
||||
]);
|
||||
|
||||
const operatorsWithoutRightOperand = [
|
||||
ZSurveyLogicConditionsOperator.Enum.isSubmitted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isSkipped,
|
||||
ZSurveyLogicConditionsOperator.Enum.isClicked,
|
||||
ZSurveyLogicConditionsOperator.Enum.isAccepted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isBooked,
|
||||
ZSurveyLogicConditionsOperator.Enum.isPartiallySubmitted,
|
||||
ZSurveyLogicConditionsOperator.Enum.isCompletelySubmitted,
|
||||
] as const;
|
||||
|
||||
export const ZDyanmicLogicField = z.enum(["question", "variable", "hiddenField"]);
|
||||
export const ZActionObjective = z.enum(["calculate", "requireAnswer", "jumpToQuestion"]);
|
||||
export const ZActionTextVariableCalculateOperator = z.enum(["assign", "concat"], {
|
||||
message: "Conditional Logic: Invalid operator for a text variable",
|
||||
});
|
||||
export const ZActionNumberVariableCalculateOperator = z.enum(
|
||||
["add", "subtract", "multiply", "divide", "assign"],
|
||||
{ message: "Conditional Logic: Invalid operator for a number variable" }
|
||||
);
|
||||
|
||||
const ZDynamicQuestion = z.object({
|
||||
type: z.literal("question"),
|
||||
value: z.string().min(1, "Conditional Logic: Question id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicVariable = z.object({
|
||||
type: z.literal("variable"),
|
||||
value: z
|
||||
.string()
|
||||
.cuid2({ message: "Conditional Logic: Variable id must be a valid cuid" })
|
||||
.min(1, "Conditional Logic: Variable id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicHiddenField = z.object({
|
||||
type: z.literal("hiddenField"),
|
||||
value: z.string().min(1, "Conditional Logic: Hidden field id cannot be empty"),
|
||||
});
|
||||
|
||||
const ZDynamicLogicFieldValue = z.union([ZDynamicQuestion, ZDynamicVariable, ZDynamicHiddenField], {
|
||||
message: "Conditional Logic: Invalid dynamic field value",
|
||||
});
|
||||
|
||||
export type TSurveyLogicConditionsOperator = z.infer<typeof ZSurveyLogicConditionsOperator>;
|
||||
export type TDyanmicLogicField = z.infer<typeof ZDyanmicLogicField>;
|
||||
export type TActionObjective = z.infer<typeof ZActionObjective>;
|
||||
export type TActionTextVariableCalculateOperator = z.infer<typeof ZActionTextVariableCalculateOperator>;
|
||||
export type TActionNumberVariableCalculateOperator = z.infer<typeof ZActionNumberVariableCalculateOperator>;
|
||||
|
||||
// Conditions
|
||||
const ZLeftOperand = ZDynamicLogicFieldValue;
|
||||
export type TLeftOperand = z.infer<typeof ZLeftOperand>;
|
||||
|
||||
export const ZRightOperandStatic = z.object({
|
||||
type: z.literal("static"),
|
||||
value: z.union([z.string(), z.number(), z.array(z.string())]),
|
||||
});
|
||||
|
||||
export const ZRightOperand = z.union([ZRightOperandStatic, ZDynamicLogicFieldValue]);
|
||||
export type TRightOperand = z.infer<typeof ZRightOperand>;
|
||||
|
||||
export const ZSingleCondition = z
|
||||
.object({
|
||||
id: ZId,
|
||||
leftOperand: ZLeftOperand,
|
||||
operator: ZSurveyLogicConditionsOperator,
|
||||
rightOperand: ZRightOperand.optional(),
|
||||
})
|
||||
.and(
|
||||
z.object({
|
||||
connector: z.undefined(),
|
||||
})
|
||||
)
|
||||
.superRefine((val, ctx) => {
|
||||
if (
|
||||
!operatorsWithoutRightOperand.includes(val.operator as (typeof operatorsWithoutRightOperand)[number])
|
||||
) {
|
||||
if (val.rightOperand === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand is required for operator "${val.operator}"`,
|
||||
path: ["rightOperand"],
|
||||
});
|
||||
} else if (val.rightOperand.type === "static" && val.rightOperand.value === "") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand value cannot be empty for operator "${val.operator}"`,
|
||||
});
|
||||
}
|
||||
} else if (val.rightOperand !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Conditional Logic: right operand should not be present for operator "${val.operator}"`,
|
||||
path: ["rightOperand"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type TSingleCondition = z.infer<typeof ZSingleCondition>;
|
||||
|
||||
export interface TConditionGroup {
|
||||
id: string;
|
||||
connector: "and" | "or";
|
||||
conditions: (TSingleCondition | TConditionGroup)[];
|
||||
}
|
||||
|
||||
const ZConditionGroup: z.ZodType<TConditionGroup> = z.lazy(() =>
|
||||
z.object({
|
||||
id: ZId,
|
||||
connector: z.enum(["and", "or"]),
|
||||
conditions: z.array(z.union([ZSingleCondition, ZConditionGroup])),
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
export const ZActionVariableValueType = z.union([z.literal("static"), ZDyanmicLogicField]);
|
||||
export type TActionVariableValueType = z.infer<typeof ZActionVariableValueType>;
|
||||
|
||||
const ZActionBase = z.object({
|
||||
id: ZId,
|
||||
objective: ZActionObjective,
|
||||
});
|
||||
|
||||
export type TActionBase = z.infer<typeof ZActionBase>;
|
||||
|
||||
const ZActionCalculateBase = ZActionBase.extend({
|
||||
objective: z.literal("calculate"),
|
||||
variableId: z.string(),
|
||||
});
|
||||
|
||||
export const ZActionCalculateText = ZActionCalculateBase.extend({
|
||||
operator: ZActionTextVariableCalculateOperator,
|
||||
value: z.union([
|
||||
z.object({
|
||||
type: z.literal("static"),
|
||||
value: z
|
||||
.string({ message: "Conditional Logic: Value must be a string for text variable" })
|
||||
.min(1, "Conditional Logic: Please enter a value in logic field"),
|
||||
}),
|
||||
ZDynamicLogicFieldValue,
|
||||
]),
|
||||
});
|
||||
|
||||
export const ZActionCalculateNumber = ZActionCalculateBase.extend({
|
||||
operator: ZActionNumberVariableCalculateOperator,
|
||||
value: z.union([
|
||||
z.object({
|
||||
type: z.literal("static"),
|
||||
value: z.number({ message: "Conditional Logic: Value must be a number for number variable" }),
|
||||
}),
|
||||
ZDynamicLogicFieldValue,
|
||||
]),
|
||||
}).superRefine((val, ctx) => {
|
||||
if (val.operator === "divide" && val.value.type === "static" && val.value.value === 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Conditional Logic: Cannot divide by zero",
|
||||
path: ["value", "value"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const ZActionCalculate = z.union([ZActionCalculateText, ZActionCalculateNumber]);
|
||||
|
||||
export type TActionCalculate = z.infer<typeof ZActionCalculate>;
|
||||
|
||||
const ZActionRequireAnswer = ZActionBase.extend({
|
||||
objective: z.literal("requireAnswer"),
|
||||
target: z.string().min(1, "Conditional Logic: Target question id cannot be empty"),
|
||||
});
|
||||
export type TActionRequireAnswer = z.infer<typeof ZActionRequireAnswer>;
|
||||
|
||||
const ZActionJumpToQuestion = ZActionBase.extend({
|
||||
objective: z.literal("jumpToQuestion"),
|
||||
target: z.string().min(1, "Conditional Logic: Target question id cannot be empty"),
|
||||
});
|
||||
|
||||
export type TActionJumpToQuestion = z.infer<typeof ZActionJumpToQuestion>;
|
||||
|
||||
export const ZSurveyLogicAction = z.union([ZActionCalculate, ZActionRequireAnswer, ZActionJumpToQuestion]);
|
||||
|
||||
export type TSurveyLogicAction = z.infer<typeof ZSurveyLogicAction>;
|
||||
|
||||
const ZSurveyLogicActions = z.array(ZSurveyLogicAction);
|
||||
|
||||
export const ZSurveyLogic = z.object({
|
||||
id: ZId,
|
||||
conditions: ZConditionGroup,
|
||||
actions: ZSurveyLogicActions,
|
||||
});
|
||||
|
||||
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
|
||||
|
||||
export const ZSurveyQuestionBase = z.object({
|
||||
id: ZSurveyQuestionId,
|
||||
type: z.string(),
|
||||
@@ -250,7 +460,7 @@ export const ZSurveyQuestionBase = z.object({
|
||||
backButtonLabel: ZI18nString.optional(),
|
||||
scale: z.enum(["number", "smiley", "star"]).optional(),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
|
||||
logic: z.array(ZSurveyAdvancedLogic).optional(),
|
||||
logic: z.array(ZSurveyLogic).optional(),
|
||||
isDraft: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -1628,7 +1838,7 @@ const validateActions = (
|
||||
survey: TSurvey,
|
||||
questionIndex: number,
|
||||
logicIndex: number,
|
||||
actions: TSurveyAdvancedLogicAction[]
|
||||
actions: TSurveyLogicAction[]
|
||||
): z.ZodIssue[] => {
|
||||
const questionIds = survey.questions.map((q) => q.id);
|
||||
|
||||
@@ -1701,11 +1911,7 @@ const validateActions = (
|
||||
return actionIssues.filter((issue) => issue !== undefined);
|
||||
};
|
||||
|
||||
const validateLogic = (
|
||||
survey: TSurvey,
|
||||
questionIndex: number,
|
||||
logic: TSurveyAdvancedLogic[]
|
||||
): z.ZodIssue[] => {
|
||||
const validateLogic = (survey: TSurvey, questionIndex: number, logic: TSurveyLogic[]): z.ZodIssue[] => {
|
||||
const logicIssues = logic.map((logicItem, logicIndex) => {
|
||||
return [
|
||||
...validateConditions(survey, questionIndex, logicIndex, logicItem.conditions),
|
||||
|
||||
@@ -2,10 +2,12 @@ import { z } from "zod";
|
||||
import type {
|
||||
TActionJumpToQuestion,
|
||||
TConditionGroup,
|
||||
TI18nString,
|
||||
TSingleCondition,
|
||||
TSurveyAdvancedLogicAction,
|
||||
} from "./logic";
|
||||
import type { TI18nString, TSurveyLanguage, TSurveyQuestion } from "./types";
|
||||
TSurveyLanguage,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
} from "./types";
|
||||
|
||||
export const FORBIDDEN_IDS = [
|
||||
"userId",
|
||||
@@ -230,7 +232,7 @@ export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestion[]): stri
|
||||
};
|
||||
|
||||
// Helper function to find all "jumpToQuestion" actions in the logic
|
||||
const findJumpToQuestionActions = (actions: TSurveyAdvancedLogicAction[]): TActionJumpToQuestion[] => {
|
||||
const findJumpToQuestionActions = (actions: TSurveyLogicAction[]): TActionJumpToQuestion[] => {
|
||||
return actions.filter((action) => action.objective === "jumpToQuestion");
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user