mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 10:36:06 -06:00
adds condition handling, added logicInput component
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
19
apps/web/app/middleware/responseHeaders.ts
Normal file
19
apps/web/app/middleware/responseHeaders.ts
Normal 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 };
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
122
packages/lib/survey/logic/utils.ts
Normal file
122
packages/lib/survey/logic/utils.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user