adds condition handling, added logicInput component

This commit is contained in:
Piyush Gupta
2024-08-21 21:39:35 +05:30
parent 4eea6a11c8
commit 29e0cf96d4
11 changed files with 775 additions and 9 deletions

View File

@@ -0,0 +1,59 @@
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 { removeAction } from "@formbricks/lib/survey/logic/utils";
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
interface AdvancedLogicEditorProps {
logicItem: TSurveyAdvancedLogic;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
questionIdx: number;
logicIdx: number;
}
export function AdvancedLogicEditor({
logicItem,
updateQuestion,
question,
questionIdx,
logicIdx,
}: AdvancedLogicEditorProps) {
const handleActionsChange = (action: "delete" | "addBelow" | "duplicate", actionIdx: number) => {
const actionsClone = structuredClone(logicItem.actions);
let updatedActions: TSurveyAdvancedLogic["actions"] = actionsClone;
if (action === "delete") {
updatedActions = removeAction(actionsClone, actionIdx);
} else if (action === "addBelow") {
updatedActions.splice(actionIdx + 1, 0, { objective: "" });
} else if (action === "duplicate") {
updatedActions.splice(actionIdx + 1, 0, actionsClone[actionIdx]);
}
updateQuestion(questionIdx, {
advancedLogic: question.advancedLogic?.map((logicItem, i) => {
if (i === logicIdx) {
return {
...logicItem,
actions: updatedActions,
};
}
return logicItem;
}),
});
};
return (
<div className="flex w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-100 p-4">
<AdvancedLogicEditorConditions
logicItem={logicItem}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
/>
<AdvancedLogicEditorActions logicItem={logicItem} handleActionsChange={handleActionsChange} />
</div>
);
}

View File

@@ -0,0 +1,78 @@
import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { removeAction } from "@formbricks/lib/survey/logic/utils";
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { Select, SelectContent, SelectTrigger } from "@formbricks/ui/Select";
interface AdvancedLogicEditorActions {
logicItem: TSurveyAdvancedLogic;
handleActionsChange: (action: "delete" | "addBelow" | "duplicate", actionIdx: number) => void;
}
export function AdvancedLogicEditorActions({ logicItem, handleActionsChange }: AdvancedLogicEditorActions) {
const actions = logicItem.actions;
return (
<div className="">
<div className="flex gap-2">
<CornerDownRightIcon className="mt-2 h-5 w-5" />
<div className="flex w-full flex-col gap-y-2">
{actions.map((action, idx) => (
<div className="flex w-full items-center justify-between gap-4">
<span>{idx === 0 ? "Then" : "and"}</span>
<Select>
<SelectTrigger></SelectTrigger>
<SelectContent></SelectContent>
</Select>
<Select>
<SelectTrigger></SelectTrigger>
<SelectContent></SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("addBelow", idx);
}}>
<PlusIcon className="h-4 w-4" />
Add action below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={actions.length === 1}
onClick={() => {
handleActionsChange("delete", idx);
}}>
<Trash2Icon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("duplicate", idx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,208 @@
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, MoreVerticalIcon, PlusIcon, Trash2Icon, WorkflowIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { performOperationsOnConditions } from "@formbricks/lib/survey/logic/utils";
import { TConditionBase, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { Select, SelectContent, SelectTrigger } from "@formbricks/ui/Select";
interface AdvancedLogicEditorConditions {
logicItem: TSurveyAdvancedLogic;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
questionIdx: number;
logicIdx: number;
}
export function AdvancedLogicEditorConditions({
logicItem,
logicIdx,
question,
questionIdx,
updateQuestion,
}: AdvancedLogicEditorConditions) {
const conditions = logicItem.conditions;
const handleAddConditionBelow = (resourceId: string, condition: TConditionBase) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
performOperationsOnConditions("addConditionBelow", advancedLogicCopy, logicIdx, resourceId, condition);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleConnectorChange = (resourceId: string, connector: TConditionBase["connector"]) => {
if (!connector) return;
console.log("onConnectorChange", resourceId, connector);
const advancedLogicCopy = structuredClone(question.advancedLogic);
performOperationsOnConditions("toggleConnector", advancedLogicCopy, logicIdx, resourceId, connector);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleRemoveCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
performOperationsOnConditions("removeCondition", advancedLogicCopy, logicIdx, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleDuplicateCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
performOperationsOnConditions("duplicateCondition", advancedLogicCopy, logicIdx, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleCreateGroup = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
performOperationsOnConditions("createGroup", advancedLogicCopy, logicIdx, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
console.log("conditions", conditions);
return (
<div className="flex flex-col gap-4 rounded-lg">
{conditions.map((condition) => {
const { connector, id, type } = condition;
if (type === "group") {
return (
<div key={id} className="flex items-start justify-between gap-4">
<div className="flex items-start gap-2">
<div className="mt-1 w-auto" key={connector}>
<span
className={cn(Boolean(connector) && "cursor-pointer underline", "text-sm")}
onClick={() => {
if (!connector) return;
handleConnectorChange(id, connector);
}}>
{connector ? connector : "When"}
</span>
</div>
</div>
<div className="w-full rounded-lg border border-slate-200 bg-slate-100 p-4">
<AdvancedLogicEditorConditions
key={id}
logicItem={condition}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
/>
</div>
<div className="mt-1">
<DropdownMenu key={`group-actions-${id}`}>
<DropdownMenuTrigger key={`group-actions-${id}`}>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={conditions.length === 1}
onClick={() => {
handleRemoveCondition(id);
}}>
<Trash2Icon className="h-4 w-4" />
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
return (
<div key={id} className="flex items-center justify-between gap-4">
<div className="flex items-start gap-2">
<div className="w-auto" key={connector}>
<span
className={cn(Boolean(connector) && "cursor-pointer underline", "text-sm")}
onClick={() => {
if (!connector) return;
handleConnectorChange(id, connector);
}}>
{connector ? connector : "When"}
</span>
</div>
</div>
<Select>
<SelectTrigger></SelectTrigger>
<SelectContent></SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleAddConditionBelow(id, {
id: createId(),
connector: "and",
});
}}>
<PlusIcon className="h-4 w-4" />
Add condition below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={conditions.length === 1}
onClick={() => {
handleRemoveCondition(id);
}}>
<Trash2Icon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleDuplicateCondition(id);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleCreateGroup(id);
}}>
<WorkflowIcon className="h-4 w-4" />
Create group
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
})}
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { LogicEditor } from "./LogicEditor";
@@ -28,6 +29,13 @@ export const AdvancedSettings = ({
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
<ConditionalLogic
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
</div>
<UpdateQuestionId

View File

@@ -0,0 +1,102 @@
import { AdvancedLogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditor";
import { createId } from "@paralleldrive/cuid2";
import { ArrowRightIcon, SplitIcon, Trash2Icon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
interface ConditionalLogicProps {
localSurvey: TSurvey;
questionIdx: number;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
}
const initialLogicState = {
id: createId(),
conditions: [
{
id: createId(),
connector: null,
},
],
actions: [{ objective: "" }],
};
export function ConditionalLogic({
attributeClasses,
localSurvey,
question,
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const addLogic = () => {
updateQuestion(questionIdx, {
advancedLogic: [...(question?.advancedLogic || []), initialLogicState],
});
};
const handleDeleteLogic = (logicItemIdx: number) => {
const advancedLogicCopy = structuredClone(question.advancedLogic || []);
advancedLogicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
return (
<div className="mt-10">
<Label className="flex gap-2">
Conditional Logic
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
{question.advancedLogic && question.advancedLogic?.length > 0 && (
<div className="logic-scrollbar mt-2 flex w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
{question.advancedLogic.map((logicItem, logicItemIdx) => (
<div key={logicItem.id} className="flex items-start gap-2">
<AdvancedLogicEditor
logicItem={logicItem}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
/>
<Button
className="mt-1 p-0"
onClick={() => {
handleDeleteLogic(logicItemIdx);
}}
variant="minimal">
<Trash2Icon className={cn("h-4 w-4 cursor-pointer")} />
</Button>
</div>
))}
</div>
)}
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
</div>
<div className="mt-2 flex items-center space-x-2">
<Button
id="logicJumps"
className="bg-slate-100 hover:bg-slate-50"
type="button"
name="logicJumps"
size="sm"
variant="secondary"
StartIcon={SplitIcon}
startIconClassName="rotate-90"
onClick={() => addLogic()}>
Add Logic
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,152 @@
import { CheckIcon, ChevronDownIcon } from "lucide-react";
import React, { ReactNode } from "react";
import { cn } from "@formbricks/lib/cn";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@formbricks/ui/Command";
import { Input } from "@formbricks/ui/Input";
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
interface LogicInputProps {
showSearch?: boolean;
searchPlaceholder?: string;
options?: { label: string | ReactNode; value: string }[];
groupedOptions?: {
label: string;
value: string;
options: { label: string | ReactNode; value: string }[];
}[];
selected: string | string[] | null;
onChangeValue: (option: string | string[]) => void;
inputProps?: React.ComponentProps<typeof Input>;
withInput?: boolean;
size?: "sm" | "lg";
allowMultiSelect?: boolean;
}
export const LogicInput = ({
showSearch = true,
searchPlaceholder = "Search...",
options,
inputProps,
groupedOptions,
selected,
onChangeValue,
withInput = false,
size = "sm",
allowMultiSelect = false,
}: LogicInputProps) => {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<string | string[] | null>(() => {
if (!selected) {
if (allowMultiSelect) {
return [];
}
}
return selected;
});
const handleSelect = (option: string) => {
if (allowMultiSelect) {
if (Array.isArray(value)) {
const newValue = value.includes(option)
? value.filter((item) => item !== option)
: [...value, option];
onChangeValue(newValue);
setValue(newValue);
} else {
onChangeValue([option]);
setValue([option]);
}
} else {
onChangeValue(option);
setValue(option);
setOpen(false);
}
};
return (
<div className="flex">
{withInput && <Input className="w-[200px] rounded-r-none border border-slate-300" {...inputProps} />}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div
role="combobox"
aria-controls="options"
aria-expanded={open}
className={cn(
"flex h-10 cursor-pointer items-center justify-center rounded-md border border-slate-300",
{
"rounded-r-none": withInput,
"w-10": size === "sm",
"w-[200px] justify-between gap-2 p-2": size === "lg",
}
)}>
{size === "lg" && (
<div>
{Array.isArray(value)
? value
.map((item) => options?.find((option) => option.value === item)?.label || item)
.join(", ")
: value || ""}
</div>
)}
<ChevronDownIcon
className="text-slate-300"
height={size === "sm" ? 20 : 16}
width={size === "sm" ? 20 : 16}
/>
</div>
</PopoverTrigger>
<PopoverContent
className={cn("w-[200px] border border-slate-400 bg-slate-50 p-0 shadow-none", {
"pt-2": size === "sm",
})}>
<Command>
{showSearch && (
<CommandInput
placeholder={searchPlaceholder}
className="h-8 border-slate-400 bg-white placeholder-slate-300"
/>
)}
<CommandList>
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options?.map((option) => (
<CommandItem key={option.value} onSelect={() => handleSelect(option.value)}>
{allowMultiSelect && Array.isArray(value) && value.includes(option.value) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300" />
)}
{option.label}
</CommandItem>
))}
</CommandGroup>
{groupedOptions?.map((group, idx) => (
<>
{idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
<CommandGroup heading={group.value}>
{group.options.map((option) => (
<CommandItem key={option.value} onSelect={() => handleSelect(option.value)}>
{allowMultiSelect && Array.isArray(value) && value.includes(option.value) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300" />
)}
{option.label}
</CommandItem>
))}
</CommandGroup>
</>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
};

View File

@@ -0,0 +1,19 @@
export const getCSPHeaderValues = () => {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, " ")
.trim();
return { nonce, cspHeader };
};

View File

@@ -653,7 +653,6 @@ function ActionSegmentFilter({
setSegment={setSegment}
viewOnly={viewOnly}
/>
<Select
disabled={viewOnly}
onValueChange={(value) => {
@@ -676,7 +675,6 @@ function ActionSegmentFilter({
))}
</SelectContent>
</Select>
<Select
disabled={viewOnly}
onValueChange={(value: TActionMetric) => {
@@ -695,7 +693,6 @@ function ActionSegmentFilter({
))}
</SelectContent>
</Select>
<Select
disabled={viewOnly}
onValueChange={(operator: TBaseOperator) => {
@@ -718,7 +715,6 @@ function ActionSegmentFilter({
))}
</SelectContent>
</Select>
<div className="relative flex flex-col gap-1">
<Input
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
@@ -734,7 +730,6 @@ function ActionSegmentFilter({
<p className="absolute right-2 -mt-1 rounded-md bg-white px-2 text-xs text-red-500">{valueError}</p>
) : null}
</div>
<SegmentFilterItemContextMenu
filterId={resource.id}
onAddFilterBelow={onAddFilterBelow}

View File

@@ -0,0 +1,122 @@
import { createId } from "@paralleldrive/cuid2";
import { TConditionBase, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
export const performOperationsOnConditions = (action, advancedLogicCopy, logicIdx, resourceId, condition) => {
const logicItem = advancedLogicCopy[logicIdx];
console.log("performOperationsOnConditions", action, resourceId, logicItem.conditions);
if (action === "addConditionBelow") {
addConditionBelow(logicItem.conditions, resourceId, condition);
} else if (action === "toggleConnector") {
console.log("toggleConnector", resourceId, logicItem.conditions);
toggleGroupConnector(logicItem.conditions, resourceId);
} else if (action === "removeCondition") {
removeCondition(logicItem.conditions, resourceId);
} else if (action === "duplicateCondition") {
duplicateCondition(logicItem.conditions, resourceId);
} else if (action === "createGroup") {
createGroupFromResource(logicItem.conditions, resourceId);
}
advancedLogicCopy[logicIdx] = {
...logicItem,
conditions: logicItem.conditions,
};
};
export const performOperationsOnActions = () => {};
export const removeAction = (actions: TSurveyAdvancedLogic["actions"], idx: number) => {
return actions.slice(0, idx).concat(actions.slice(idx + 1));
};
// write the recursive function below, check if the conditions is of type group
export const addConditionBelow = (
group: TSurveyAdvancedLogic["conditions"],
resourceId: string,
condition: TConditionBase
) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (type !== "group") {
if (id === resourceId) {
group.splice(i + 1, 0, condition);
break;
}
} else {
if (group[i].id === resourceId) {
group.splice(i + 1, 0, condition);
break;
} else {
if (type === "group") {
addConditionBelow(group[i].conditions, resourceId, condition);
}
}
}
}
};
export const toggleGroupConnector = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (id === resourceId) {
console.log("madarchod", group[i].connector);
group[i].connector = group[i].connector === "and" ? "or" : "and";
return;
}
if (type === "group") toggleGroupConnector(group[i].conditions, resourceId);
}
};
export const removeCondition = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (id === resourceId) {
if (i === 0) group[i + 1].connector = null;
group.splice(i, 1);
return;
}
if (type === "group") removeCondition(group[i].conditions, resourceId);
}
};
export const duplicateCondition = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (id === resourceId) {
group.splice(i + 1, 0, {
...group[i],
id: createId(),
connector: i === 0 ? "and" : group[i].connector,
});
return;
}
if (type === "group") duplicateCondition(group[i].conditions, resourceId);
}
};
export const createGroupFromResource = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (id === resourceId) {
group[i] = {
id: createId(),
type: "group",
connector: group.length === 1 ? null : group[i].connector || "and",
conditions: [{ ...group[i], connector: null }],
};
return;
}
if (type === "group") createGroupFromResource(group[i].conditions, resourceId);
}
};

View File

@@ -1,5 +1,24 @@
import { z } from "zod";
import { TSurveyQuestionTypeEnum, ZSurveyOpenTextQuestionInputType } from "./types";
// import { TSurveyQuestionTypeEnum, ZSurveyOpenTextQuestionInputType } from "./types";
export const ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url", "number", "phone"]);
export enum TSurveyQuestionTypeEnum {
FileUpload = "fileUpload",
OpenText = "openText",
MultipleChoiceSingle = "multipleChoiceSingle",
MultipleChoiceMulti = "multipleChoiceMulti",
NPS = "nps",
CTA = "cta",
Rating = "rating",
Consent = "consent",
PictureSelection = "pictureSelection",
Cal = "cal",
Date = "date",
Matrix = "matrix",
Address = "address",
}
export const ZSurveyLogicCondition = z.enum([
"equals",
@@ -59,6 +78,8 @@ const ZConditionBase = z.object({
matchValue: ZMatchValue,
});
export type TConditionBase = z.infer<typeof ZConditionBase>;
const ZConditionQuestionBase = ZConditionBase.extend({
type: z.literal("question"),
questionType: z.nativeEnum(TSurveyQuestionTypeEnum),
@@ -363,14 +384,14 @@ const ZGroupedConditions: z.ZodType<TGroupedConditions> = z.object({
conditions: z.array(z.union([ZCondition, z.lazy(() => ZGroupedConditions)])),
});
const ZSurveyLogic = z.object({
export const ZSurveyAdvancedLogic = z.object({
id: z.string().cuid2(),
conditions: z.array(z.union([ZCondition, ZGroupedConditions])),
actions: z.array(ZAction),
});
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
export type TSurveyAdvancedLogic = z.infer<typeof ZSurveyAdvancedLogic>;
export const ZSurveyQuestionBase = z.object({
logic: z.array(ZSurveyLogic).optional(),
logic: z.array(ZSurveyAdvancedLogic).optional(),
});

View File

@@ -6,6 +6,7 @@ import { ZId } from "../environment";
import { ZLanguage } from "../product";
import { ZSegment } from "../segment";
import { ZBaseStyling } from "../styling";
import { ZSurveyAdvancedLogic } from "./logic";
import {
FORBIDDEN_IDS,
findLanguageCodesForDuplicateLabels,
@@ -334,6 +335,7 @@ export const ZSurveyQuestionBase = z.object({
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(ZSurveyLogic).optional(),
advancedLogic: z.array(ZSurveyAdvancedLogic).optional(),
isDraft: z.boolean().optional(),
});