adds local schema updation handlers

This commit is contained in:
Piyush Gupta
2024-08-23 15:43:34 +05:30
parent 245972234e
commit 1ba885e5dc
15 changed files with 1150 additions and 239 deletions

View File

@@ -1,8 +1,8 @@
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 { createId } from "@paralleldrive/cuid2";
import { removeAction } from "@formbricks/lib/survey/logic/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TAction, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
interface AdvancedLogicEditorProps {
@@ -12,9 +12,7 @@ interface AdvancedLogicEditorProps {
question: TSurveyQuestion;
questionIdx: number;
logicIdx: number;
hiddenFields: string[];
userAttributes: string[];
attributeClasses: TAttributeClass[];
}
export function AdvancedLogicEditor({
@@ -24,20 +22,27 @@ export function AdvancedLogicEditor({
question,
questionIdx,
logicIdx,
hiddenFields,
userAttributes,
attributeClasses,
}: AdvancedLogicEditorProps) {
const handleActionsChange = (action: "delete" | "addBelow" | "duplicate", actionIdx: number) => {
const handleActionsChange = (
operation: "delete" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: Partial<TAction>
) => {
const actionsClone = structuredClone(logicItem.actions);
let updatedActions: TSurveyAdvancedLogic["actions"] = actionsClone;
if (action === "delete") {
if (operation === "delete") {
updatedActions = removeAction(actionsClone, actionIdx);
} else if (action === "addBelow") {
updatedActions.splice(actionIdx + 1, 0, { objective: "" });
} else if (action === "duplicate") {
} else if (operation === "addBelow") {
updatedActions.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" });
} else if (operation === "duplicate") {
updatedActions.splice(actionIdx + 1, 0, actionsClone[actionIdx]);
} else if (operation === "update") {
updatedActions[actionIdx] = {
...updatedActions[actionIdx],
...action,
};
}
updateQuestion(questionIdx, {
@@ -60,18 +65,16 @@ export function AdvancedLogicEditor({
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
localSurvey={localSurvey}
logicIdx={logicIdx}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
<AdvancedLogicEditorActions
logicItem={logicItem}
handleActionsChange={handleActionsChange}
hiddenFields={hiddenFields}
localSurvey={localSurvey}
userAttributes={userAttributes}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
</div>
);

View File

@@ -1,14 +1,20 @@
import {
getOpeartorOptions,
getTargetOptions,
getValueOptions,
getActionOpeartorOptions,
getActionTargetOptions,
getActionValueOptions,
getActionVariableOptions,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useMemo } from "react";
import { actionObjectiveOptions } 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 {
TAction,
TActionCalculateVariableType,
TActionNumberVariableCalculateOperator,
TActionObjective,
TActionTextVariableCalculateOperator,
TDyanmicLogicField,
TSurveyAdvancedLogic,
} from "@formbricks/types/surveys/logic";
import { TSurvey } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
@@ -21,27 +27,29 @@ import { InputCombobox } from "@formbricks/ui/InputCombobox";
interface AdvancedLogicEditorActions {
localSurvey: TSurvey;
logicItem: TSurveyAdvancedLogic;
handleActionsChange: (action: "delete" | "addBelow" | "duplicate", actionIdx: number) => void;
hiddenFields: string[];
handleActionsChange: (
operation: "delete" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: Partial<TAction>
) => void;
userAttributes: string[];
questionIdx: number;
attributeClasses: TAttributeClass[];
}
export function AdvancedLogicEditorActions({
localSurvey,
logicItem,
handleActionsChange,
hiddenFields,
userAttributes,
questionIdx,
attributeClasses,
}: AdvancedLogicEditorActions) {
const actions = logicItem.actions;
const transformedSurvey = useMemo(() => {
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
}, [localSurvey, attributeClasses]);
const updateAction = (actionIdx: number, updatedAction: Partial<TAction>) => {
handleActionsChange("update", actionIdx, updatedAction);
};
console.log("actions", actions);
return (
<div className="">
<div className="flex gap-2">
@@ -56,38 +64,82 @@ export function AdvancedLogicEditorActions({
showSearch={false}
options={actionObjectiveOptions}
selected={action.objective}
onChangeValue={() => {}}
onChangeValue={(val: TActionObjective) => {
updateAction(idx, {
objective: val,
target: "",
operator: undefined,
variableType: undefined,
});
}}
comboboxClasses="max-w-[200px]"
/>
<InputCombobox
key="target"
showSearch={false}
options={getTargetOptions(transformedSurvey.questions, questionIdx)}
selected={action.objective}
onChangeValue={() => {}}
comboboxClasses="grow"
options={
action.objective === "calculate"
? getActionVariableOptions(localSurvey)
: getActionTargetOptions(localSurvey, questionIdx)
}
selected={action.target}
onChangeValue={(val: string, option) => {
updateAction(idx, {
target: val,
variableType: option?.meta?.variableType as TActionCalculateVariableType,
});
}}
comboboxClasses="grow min-w-[100px]"
/>
{action.objective === "calculate" && (
<>
<InputCombobox
key="attribute"
showSearch={false}
options={getOpeartorOptions(action.variableType)}
options={getActionOpeartorOptions(action.variableType)}
selected={action.operator}
onChangeValue={() => {}}
onChangeValue={(
val: TActionNumberVariableCalculateOperator | TActionTextVariableCalculateOperator
) => {
updateAction(idx, {
operator: val,
});
}}
comboboxClasses="min-w-[100px]"
/>
<InputCombobox
key="value"
withInput={true}
inputProps={{ placeholder: "Value" }}
groupedOptions={getValueOptions(
transformedSurvey.questions,
questionIdx,
hiddenFields,
userAttributes
)}
onChangeValue={() => {}}
comboboxClasses="flex"
inputProps={{
placeholder: "Value",
value: typeof action.value !== "object" ? action.value : "",
type: action.variableType,
onChange: (e) => {
let val: string | number = e.target.value;
if (action.variableType === "number") {
val = Number(val);
updateAction(idx, {
value: val,
});
} else if (action.variableType === "text") {
updateAction(idx, {
value: val,
});
}
},
}}
groupedOptions={getActionValueOptions(localSurvey, questionIdx, userAttributes)}
onChangeValue={(val: string, option) => {
updateAction(idx, {
value: {
id: val,
fieldType: option?.meta?.fieldType as TDyanmicLogicField,
type: "dynamic",
},
});
}}
comboboxClasses="flex min-w-[100px]"
comboboxSize="sm"
/>
</>

View File

@@ -1,24 +1,29 @@
import {
getConditionOperatorOptions,
getConditionValueOptions,
getMatchValueProps,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
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 { TConditionBase, TSurveyAdvancedLogic, TSurveyLogicCondition } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { Select, SelectContent, SelectTrigger } from "@formbricks/ui/Select";
import { InputCombobox } from "@formbricks/ui/InputCombobox";
interface AdvancedLogicEditorConditions {
conditions: TSurveyAdvancedLogic["conditions"];
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
localSurvey: TSurvey;
questionIdx: number;
logicIdx: number;
hiddenFields: string[];
userAttributes: string[];
}
@@ -26,15 +31,21 @@ export function AdvancedLogicEditorConditions({
conditions,
logicIdx,
question,
localSurvey,
questionIdx,
updateQuestion,
hiddenFields,
userAttributes,
}: AdvancedLogicEditorConditions) {
const handleAddConditionBelow = (resourceId: string, condition: TConditionBase) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
const handleAddConditionBelow = (resourceId: string, condition: Partial<TConditionBase>) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions("addConditionBelow", advancedLogicCopy, logicIdx, resourceId, condition);
performOperationsOnConditions({
action: "addConditionBelow",
advancedLogicCopy,
logicIdx,
resourceId,
condition,
});
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -43,10 +54,14 @@ export function AdvancedLogicEditorConditions({
const handleConnectorChange = (resourceId: string, connector: TConditionBase["connector"]) => {
if (!connector) return;
console.log("onConnectorChange", resourceId, connector);
const advancedLogicCopy = structuredClone(question.advancedLogic);
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions("toggleConnector", advancedLogicCopy, logicIdx, resourceId, connector);
performOperationsOnConditions({
action: "toggleConnector",
advancedLogicCopy,
logicIdx,
resourceId,
});
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -54,9 +69,14 @@ export function AdvancedLogicEditorConditions({
};
const handleRemoveCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions("removeCondition", advancedLogicCopy, logicIdx, resourceId);
performOperationsOnConditions({
action: "removeCondition",
advancedLogicCopy,
logicIdx,
resourceId,
});
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -64,9 +84,9 @@ export function AdvancedLogicEditorConditions({
};
const handleDuplicateCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions("duplicateCondition", advancedLogicCopy, logicIdx, resourceId);
performOperationsOnConditions({ action: "duplicateCondition", advancedLogicCopy, logicIdx, resourceId });
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -74,9 +94,30 @@ export function AdvancedLogicEditorConditions({
};
const handleCreateGroup = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic);
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions("createGroup", advancedLogicCopy, logicIdx, resourceId);
performOperationsOnConditions({
action: "createGroup",
advancedLogicCopy,
logicIdx,
resourceId,
});
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleUpdateCondition = (resourceId: string, updateConditionBody: Partial<TConditionBase>) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({
action: "updateCondition",
advancedLogicCopy,
logicIdx,
resourceId,
conditionBody: updateConditionBody,
});
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -110,10 +151,10 @@ export function AdvancedLogicEditorConditions({
conditions={condition.conditions}
key={id}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
</div>
@@ -140,6 +181,9 @@ export function AdvancedLogicEditorConditions({
);
}
const conditionValueOptions = getConditionValueOptions(localSurvey, questionIdx, userAttributes);
const conditionOperatorOptions = getConditionOperatorOptions(condition);
const { show, options } = getMatchValueProps(localSurvey, condition, questionIdx, userAttributes);
return (
<div key={id} className="flex items-center justify-between gap-4">
<div className="flex items-start gap-2">
@@ -154,10 +198,43 @@ export function AdvancedLogicEditorConditions({
</span>
</div>
</div>
<Select>
<SelectTrigger></SelectTrigger>
<SelectContent></SelectContent>
</Select>
<InputCombobox
key="conditionValue"
showSearch={false}
groupedOptions={conditionValueOptions}
selected={condition.conditionValue}
onChangeValue={(val: string, option) => {
handleUpdateCondition(id, {
conditionValue: val,
...option?.meta,
});
}}
comboboxClasses="grow"
/>
<InputCombobox
key="conditionOperator"
showSearch={false}
options={conditionOperatorOptions}
selected={condition.conditionOperator}
onChangeValue={(val: TSurveyLogicCondition, option) => {
console.log("val", val, option);
handleUpdateCondition(id, {
conditionOperator: val,
});
}}
comboboxClasses="grow"
/>
{show && options.length > 0 && (
<InputCombobox
withInput
key="conditionMatchValue"
showSearch={false}
groupedOptions={options}
comboboxSize="sm"
selected={condition.conditionValue}
onChangeValue={() => {}}
/>
)}
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVerticalIcon className="h-4 w-4" />
@@ -169,8 +246,10 @@ export function AdvancedLogicEditorConditions({
onClick={() => {
handleAddConditionBelow(id, {
id: createId(),
connector: "and",
conditionValue: localSurvey.questions[questionIdx].id,
type: "question",
questionType: localSurvey.questions[questionIdx].type,
});
}}>
<PlusIcon className="h-4 w-4" />

View File

@@ -10,7 +10,6 @@ interface AdvancedSettingsProps {
localSurvey: TSurvey;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
hiddenFields: string[];
userAttributes: string[];
}
@@ -20,7 +19,6 @@ export const AdvancedSettings = ({
localSurvey,
updateQuestion,
attributeClasses,
hiddenFields,
userAttributes,
}: AdvancedSettingsProps) => {
return (
@@ -39,7 +37,6 @@ export const AdvancedSettings = ({
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
</div>

View File

@@ -1,8 +1,11 @@
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 { useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
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 { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
@@ -13,7 +16,6 @@ interface ConditionalLogicProps {
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
hiddenFields: string[];
userAttributes: string[];
}
@@ -34,12 +36,36 @@ export function ConditionalLogic({
question,
questionIdx,
updateQuestion,
hiddenFields,
userAttributes,
}: ConditionalLogicProps) {
const transformedSurvey = useMemo(() => {
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
}, [localSurvey, attributeClasses]);
const addLogic = () => {
const condition: TSurveyAdvancedLogic = {
id: createId(),
conditions: [
{
id: createId(),
connector: null,
type: "question",
conditionValue: question.id,
questionType: question.type,
matchValue: null,
},
],
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: "",
},
],
};
updateQuestion(questionIdx, {
advancedLogic: [...(question?.advancedLogic || []), initialLogicState],
advancedLogic: [...(question?.advancedLogic || []), condition],
});
};
@@ -63,15 +89,13 @@ export function ConditionalLogic({
{question.advancedLogic.map((logicItem, logicItemIdx) => (
<div key={logicItem.id} className="flex items-start gap-2">
<AdvancedLogicEditor
localSurvey={localSurvey}
localSurvey={transformedSurvey}
logicItem={logicItem}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
userAttributes={userAttributes}
hiddenFields={hiddenFields}
attributeClasses={attributeClasses}
/>
<Button
className="mt-1 p-0"

View File

@@ -53,7 +53,6 @@ interface QuestionCardProps {
attributeClasses: TAttributeClass[];
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
hiddenFields: string[];
userAttributes: string[];
}
@@ -75,7 +74,6 @@ export const QuestionCard = ({
attributeClasses,
addQuestion,
isFormbricksCloud,
hiddenFields,
userAttributes,
}: QuestionCardProps) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -459,7 +457,6 @@ export const QuestionCard = ({
localSurvey={localSurvey}
updateQuestion={updateQuestion}
attributeClasses={attributeClasses}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
</Collapsible.CollapsibleContent>

View File

@@ -20,7 +20,6 @@ interface QuestionsDraggableProps {
attributeClasses: TAttributeClass[];
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
hiddenFields: string[];
userAttributes: string[];
}
@@ -40,7 +39,6 @@ export const QuestionsDroppable = ({
attributeClasses,
addQuestion,
isFormbricksCloud,
hiddenFields,
userAttributes,
}: QuestionsDraggableProps) => {
return (
@@ -66,7 +64,6 @@ export const QuestionsDroppable = ({
attributeClasses={attributeClasses}
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
))}

View File

@@ -1,6 +1,7 @@
"use client";
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 {
DndContext,
DragEndEvent,
@@ -49,7 +50,6 @@ interface QuestionsViewProps {
isFormbricksCloud: boolean;
attributeClasses: TAttributeClass[];
plan: TOrganizationBillingPlan;
hiddenFields: string[];
userAttributes: string[];
}
@@ -67,7 +67,6 @@ export const QuestionsView = ({
isFormbricksCloud,
attributeClasses,
plan,
hiddenFields,
userAttributes,
}: QuestionsViewProps) => {
const internalQuestionIdMap = useMemo(() => {
@@ -393,7 +392,6 @@ export const QuestionsView = ({
attributeClasses={attributeClasses}
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
</DndContext>
@@ -439,12 +437,12 @@ export const QuestionsView = ({
activeQuestionId={activeQuestionId}
/>
{/* <SurveyVariablesCard
<SurveyVariablesCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
/> */}
/>
<MultiLanguageCard
localSurvey={localSurvey}

View File

@@ -37,7 +37,6 @@ interface SurveyEditorProps {
isFormbricksCloud: boolean;
isUnsplashConfigured: boolean;
plan: TOrganizationBillingPlan;
hiddenFields: string[];
userAttributes: string[];
}
@@ -57,7 +56,6 @@ export const SurveyEditor = ({
isFormbricksCloud,
isUnsplashConfigured,
plan,
hiddenFields,
userAttributes,
}: SurveyEditorProps) => {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
@@ -174,7 +172,6 @@ export const SurveyEditor = ({
isFormbricksCloud={isFormbricksCloud}
attributeClasses={attributeClasses}
plan={plan}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
)}

View File

@@ -0,0 +1,410 @@
import { TSurveyQuestionTypeEnum, ZSurveyLogicCondition } from "@formbricks/types/surveys/logic";
export const ruleEngine = {
question: {
[TSurveyQuestionTypeEnum.OpenText]: {
text: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: "contains",
value: ZSurveyLogicCondition.Enum.contains,
},
{
label: "does not contain",
value: ZSurveyLogicCondition.Enum.doesNotContain,
},
{
label: "starts with",
value: ZSurveyLogicCondition.Enum.startsWith,
},
{
label: "does not start with",
value: ZSurveyLogicCondition.Enum.doesNotStartWith,
},
{
label: "ends with",
value: ZSurveyLogicCondition.Enum.endsWith,
},
{
label: "does not end with",
value: ZSurveyLogicCondition.Enum.doesNotEndWith,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
number: {
options: [
{
label: "=",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicCondition.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicCondition.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicCondition.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
},
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: "equals one of",
value: ZSurveyLogicCondition.Enum.equalsOneOf,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: "includes all of",
value: ZSurveyLogicCondition.Enum.includesAllOf,
},
{
label: "includes one of",
value: ZSurveyLogicCondition.Enum.includesOneOf,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.PictureSelection]: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: "includes all of",
value: ZSurveyLogicCondition.Enum.includesAllOf,
},
{
label: "includes one of",
value: ZSurveyLogicCondition.Enum.includesOneOf,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Rating]: {
options: [
{
label: "=",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicCondition.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicCondition.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicCondition.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.NPS]: {
options: [
{
label: "=",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicCondition.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicCondition.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicCondition.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.CTA]: {
options: [
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Consent]: {
options: [
{
label: "is accepted",
value: ZSurveyLogicCondition.Enum.isAccepted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Date]: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: "is before",
value: ZSurveyLogicCondition.Enum.isBefore,
},
{
label: "is after",
value: ZSurveyLogicCondition.Enum.isAfter,
},
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.FileUpload]: {
options: [
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Cal]: {
options: [
{
label: "is booked",
value: ZSurveyLogicCondition.Enum.isBooked,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Matrix]: {
options: [
{
label: "is partially submitted",
value: ZSurveyLogicCondition.Enum.isPartiallySubmitted,
},
{
label: "is completely submitted",
value: ZSurveyLogicCondition.Enum.isCompletelySubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
[TSurveyQuestionTypeEnum.Address]: {
options: [
{
label: "is submitted",
value: ZSurveyLogicCondition.Enum.isSubmitted,
},
{
label: "is skipped",
value: ZSurveyLogicCondition.Enum.isSkipped,
},
],
},
},
variable: {
text: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
],
},
number: {
options: [
{
label: "=",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "!=",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
{
label: ">",
value: ZSurveyLogicCondition.Enum.isGreaterThan,
},
{
label: "<",
value: ZSurveyLogicCondition.Enum.isLessThan,
},
{
label: ">=",
value: ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
},
{
label: "<=",
value: ZSurveyLogicCondition.Enum.isLessThanOrEqual,
},
],
},
},
hiddenField: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
],
},
userAttribute: {
options: [
{
label: "equals",
value: ZSurveyLogicCondition.Enum.equals,
},
{
label: "does not equal",
value: ZSurveyLogicCondition.Enum.doesNotEqual,
},
],
},
};

View File

@@ -1,7 +1,11 @@
import { ruleEngine } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/logicRuleEngine";
import {
ArrowUpFromLineIcon,
CalendarDaysIcon,
CheckIcon,
EyeOffIcon,
FileDigitIcon,
FileType2Icon,
Grid3X3Icon,
HomeIcon,
ImageIcon,
@@ -12,14 +16,17 @@ import {
PresentationIcon,
Rows3Icon,
StarIcon,
TagIcon,
} from "lucide-react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import {
TActionCalculateVariableType,
TActionNumberVariableCalculateOperator,
TActionTextVariableCalculateOperator,
TCondition,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/logic";
import { TSurveyQuestions } from "@formbricks/types/surveys/types";
import { TSurvey } from "@formbricks/types/surveys/types";
import { ComboboxGroupedOption, ComboboxOption } from "@formbricks/ui/InputCombobox";
// formats the text to highlight specific parts of the text with slashes
@@ -57,8 +64,266 @@ const questionIconMapping = {
address: HomeIcon,
};
export const getTargetOptions = (questions: TSurveyQuestions, currQuestionIdx: number): ComboboxOption[] => {
return questions
export const getConditionValueOptions = (
localSurvey: TSurvey,
currQuestionIdx: number,
userAttributes: string[]
): ComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds || [];
const variables = localSurvey.variables || [];
const questions = localSurvey.questions;
const groupedOptions: ComboboxGroupedOption[] = [];
const questionOptions = questions
.filter((_, idx) => idx <= currQuestionIdx)
.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
questionType: question.type,
inputType: question.type === "openText" ? question.inputType : "",
},
};
});
const variableOptions = variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
variableType: variable.type,
},
};
});
const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});
const userAttributesOptions = userAttributes.map((attribute) => {
return {
icon: TagIcon,
label: attribute,
value: attribute,
meta: {
type: "userAttribute",
},
};
});
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
if (userAttributesOptions.length > 0) {
groupedOptions.push({
label: "User Attributes",
value: "userAttributes",
options: userAttributesOptions,
});
}
return groupedOptions;
};
export const getConditionOperatorOptions = (condition: TCondition): ComboboxOption[] => {
if (condition.type === "attributeClass") {
return ruleEngine.userAttribute.options;
} else if (condition.type === "variable") {
return ruleEngine.variable[condition.variableType].options;
} else if (condition.type === "hiddenField") {
return ruleEngine.hiddenField.options;
} else if (condition.type === "question") {
if (condition.questionType === "openText") {
const inputType = condition.inputType === "number" ? "number" : "text";
return ruleEngine.question.openText[inputType].options;
}
return ruleEngine.question[condition.questionType].options;
}
return [];
};
export const getMatchValueProps = (
localSurvey: TSurvey,
condition: TCondition,
questionIdx: number,
userAttributes: string[]
): { show: boolean; options: ComboboxGroupedOption[] } => {
if (
[
"isAccepted",
"isBooked",
"isClicked",
"isCompletelySubmitted",
"isPartiallySubmitted",
"isSkipped",
"isSubmitted",
].includes(condition.conditionOperator)
) {
return { show: false, options: [] };
}
let questionOptions = localSurvey.questions
.filter((_, idx) => idx !== questionIdx)
.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
fieldType: "question",
},
};
});
let variableOptions = localSurvey.variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
fieldType: "variable",
},
};
});
let hiddenFieldsOptions =
localSurvey?.hiddenFields?.fieldIds?.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
fieldType: "hiddenField",
},
};
}) || [];
let userAttributesOptions = userAttributes.map((attribute) => {
return {
icon: TagIcon,
label: attribute,
value: attribute,
meta: {
fieldType: "userAttribute",
},
};
});
const groupedOptions: ComboboxGroupedOption[] = [];
if (condition.type === "hiddenField") {
hiddenFieldsOptions = hiddenFieldsOptions?.filter((field) => field.value !== condition.conditionValue);
} else if (condition.type === "variable") {
variableOptions = variableOptions?.filter((variable) => variable.value !== condition.conditionValue);
} else if (condition.type === "attributeClass") {
userAttributesOptions = userAttributesOptions?.filter(
(attribute) => attribute.value !== condition.conditionValue
);
} else if (condition.type === "question") {
questionOptions = questionOptions?.filter((question) => question.value !== condition.conditionValue);
const question = localSurvey.questions.find((question) => question.id === condition.conditionValue);
let choices: ComboboxOption[] = [];
if (
question &&
(question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti)
) {
choices = question.choices.map((choice) => ({
label: getLocalizedValue(choice.label, "default"),
value: choice.id,
}));
}
if (question && question.type === TSurveyQuestionTypeEnum.PictureSelection) {
choices = question.choices.map((choice, idx) => ({
label: choice.imageUrl.split("/").pop() || `Image ${idx + 1}`,
value: choice.id,
}));
}
if (choices.length > 0) {
groupedOptions.push({
label: "Choices",
value: "choices",
options: choices,
});
}
}
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
if (userAttributesOptions.length > 0) {
groupedOptions.push({
label: "User Attributes",
value: "userAttributes",
options: userAttributesOptions,
});
}
return { show: true, options: groupedOptions };
// const question = localSurvey.questions[questionIdx];
return { show: true, options: [] };
};
export const getActionTargetOptions = (localSurvey: TSurvey, currQuestionIdx: number): ComboboxOption[] => {
const questionOptions = localSurvey.questions
.filter((_, idx) => idx !== currQuestionIdx)
.map((question) => {
return {
@@ -67,9 +332,36 @@ export const getTargetOptions = (questions: TSurveyQuestions, currQuestionIdx: n
value: question.id,
};
});
const endingCardOptions = localSurvey.endings.map((ending) => {
return {
label:
ending.type === "endScreen"
? `🙏${getLocalizedValue(ending.headline, "default")}`
: `🙏 ${ending.label || "Redirect Thank you card"}`,
value: ending.id,
};
});
return [...questionOptions, ...endingCardOptions];
};
export const getOpeartorOptions = (variableType: TActionCalculateVariableType): ComboboxOption[] => {
export const getActionVariableOptions = (localSurvey: TSurvey): ComboboxOption[] => {
const variables = localSurvey.variables || [];
return variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
variableType: variable.type,
},
};
});
};
export const getActionOpeartorOptions = (variableType: TActionCalculateVariableType): ComboboxOption[] => {
if (variableType === TActionCalculateVariableType.Number) {
return [
{
@@ -108,26 +400,61 @@ export const getOpeartorOptions = (variableType: TActionCalculateVariableType):
return [];
};
export const getValueOptions = (
questions: TSurveyQuestions,
export const getActionValueOptions = (
localSurvey: TSurvey,
currQuestionIdx: number,
hiddenFields: string[],
userAttributes: string[]
): ComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds || [];
const variables = localSurvey.variables || [];
const questions = localSurvey.questions;
const groupedOptions: ComboboxGroupedOption[] = [];
const questionOptions = getTargetOptions(questions, currQuestionIdx);
// const questionOptions = getActionTargetOptions(questions, currQuestionIdx);
const questionOptions = questions
.filter((_, idx) => idx !== currQuestionIdx)
.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
fieldType: "question",
},
};
});
const variableOptions = variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
fieldType: "variable",
},
};
});
const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
fieldType: "hiddenField",
},
};
});
const userAttributesOptions = userAttributes.map((attribute) => {
return {
icon: TagIcon,
label: attribute,
value: attribute,
meta: {
fieldType: "userAttribute",
},
};
});
@@ -139,6 +466,14 @@ export const getValueOptions = (
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",

View File

@@ -33,7 +33,7 @@ const Page = async ({ params }) => {
organization,
session,
segments,
{ hiddenFields, userAttributes },
{ userAttributes },
] = await Promise.all([
getSurvey(params.surveyId),
getProductByEnvironmentId(params.environmentId),
@@ -89,7 +89,6 @@ const Page = async ({ params }) => {
plan={organization.billing.plan}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
hiddenFields={hiddenFields}
userAttributes={userAttributes}
/>
);

View File

@@ -1,17 +1,31 @@
import { createId } from "@paralleldrive/cuid2";
import {
TActionBase,
TActionObjective,
TConditionBase,
TSurveyAdvancedLogic,
} from "@formbricks/types/surveys/logic";
import { TActionObjective, TConditionBase, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
export const performOperationsOnConditions = (action, advancedLogicCopy, logicIdx, resourceId, condition) => {
export const performOperationsOnConditions = ({
action,
advancedLogicCopy,
logicIdx,
resourceId,
condition,
conditionBody,
}: {
action:
| "addConditionBelow"
| "toggleConnector"
| "removeCondition"
| "duplicateCondition"
| "createGroup"
| "updateCondition";
advancedLogicCopy: TSurveyAdvancedLogic[];
logicIdx: number;
resourceId: string;
condition?: TConditionBase;
conditionBody?: Partial<TConditionBase>;
}) => {
const logicItem = advancedLogicCopy[logicIdx];
console.log("performOperationsOnConditions", action, resourceId, logicItem.conditions);
if (action === "addConditionBelow") {
if (!condition) return;
addConditionBelow(logicItem.conditions, resourceId, condition);
} else if (action === "toggleConnector") {
console.log("toggleConnector", resourceId, logicItem.conditions);
@@ -22,6 +36,9 @@ export const performOperationsOnConditions = (action, advancedLogicCopy, logicId
duplicateCondition(logicItem.conditions, resourceId);
} else if (action === "createGroup") {
createGroupFromResource(logicItem.conditions, resourceId);
} else if (action === "updateCondition") {
if (!conditionBody) return;
updateCondition(logicItem.conditions, resourceId, conditionBody);
}
advancedLogicCopy[logicIdx] = {
@@ -30,13 +47,10 @@ export const performOperationsOnConditions = (action, advancedLogicCopy, logicId
};
};
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,
@@ -126,6 +140,23 @@ export const createGroupFromResource = (group: TSurveyAdvancedLogic["conditions"
}
};
export const updateCondition = (
group: TSurveyAdvancedLogic["conditions"],
resourceId: string,
condition: Partial<TConditionBase>
) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
if (id === resourceId) {
group[i] = { ...group[i], ...condition };
return;
}
if (type === "group") updateCondition(group[i].conditions, resourceId, condition);
}
};
export const actionObjectiveOptions: { label: string; value: TActionObjective }[] = [
{ label: "Calculate", value: TActionObjective.Calculate },
{ label: "Require Answer", value: TActionObjective.RequireAnswer },

View File

@@ -47,7 +47,11 @@ export const ZSurveyLogicCondition = z.enum([
"isCompletelySubmitted",
]);
const ZDyanmicLogicField = z.enum(["question", "variable", "attributeClass", "hiddenField"]);
export type TSurveyLogicCondition = z.infer<typeof ZSurveyLogicCondition>;
export const ZDyanmicLogicField = z.enum(["question", "variable", "attributeClass", "hiddenField"]);
export type TDyanmicLogicField = z.infer<typeof ZDyanmicLogicField>;
const ZMatchValueBase = z.object({
type: z.enum(["static", "dynamic"]),
@@ -74,8 +78,8 @@ const ZConditionBase = z.object({
connector: ZLogicalConnector.nullable(),
type: ZDyanmicLogicField,
conditionValue: z.string(),
conditionOperator: ZSurveyLogicCondition,
matchValue: ZMatchValue,
conditionOperator: ZSurveyLogicCondition.nullable(),
matchValue: ZMatchValue.nullable(),
});
export type TConditionBase = z.infer<typeof ZConditionBase>;
@@ -85,44 +89,41 @@ const ZConditionQuestionBase = ZConditionBase.extend({
questionType: z.nativeEnum(TSurveyQuestionTypeEnum),
});
export type TConditionQuestionBase = z.infer<typeof ZConditionQuestionBase>;
const ZOpenTextConditionBase = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.OpenText),
inputType: ZSurveyOpenTextQuestionInputType.optional().default("text"),
});
const ZOpenTextStringConditon = ZOpenTextConditionBase.extend({
inputType: z.enum([
ZSurveyOpenTextQuestionInputType.enum.text,
ZSurveyOpenTextQuestionInputType.enum.email,
ZSurveyOpenTextQuestionInputType.enum.url,
ZSurveyOpenTextQuestionInputType.enum.phone,
]),
inputType: z.enum(["text", "email", "url", "phone"]),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.contains,
ZSurveyLogicCondition.Enum.doesNotContain,
ZSurveyLogicCondition.Enum.startsWith,
ZSurveyLogicCondition.Enum.doesNotStartWith,
ZSurveyLogicCondition.Enum.endsWith,
ZSurveyLogicCondition.Enum.doesNotEndWith,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
"startsWith",
"doesNotStartWith",
"endsWith",
"doesNotEndWith",
"isSubmitted",
"isSkipped",
]),
matchValue: z.string().optional(),
});
const ZOpenTextNumberConditon = ZOpenTextConditionBase.extend({
inputType: z.literal(ZSurveyOpenTextQuestionInputType.enum.number),
inputType: z.literal("number"),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.isGreaterThan,
ZSurveyLogicCondition.Enum.isLessThan,
ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
ZSurveyLogicCondition.Enum.isLessThanOrEqual,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"isGreaterThan",
"isLessThan",
"isGreaterThanOrEqual",
"isLessThanOrEqual",
"isSubmitted",
"isSkipped",
]),
matchValue: z.number().optional(),
});
@@ -131,50 +132,44 @@ const ZOpenTextCondition = z.union([ZOpenTextStringConditon, ZOpenTextNumberCond
const ZMultipleChoiceSingleCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceSingle),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.equalsOneOf,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
]),
conditionOperator: z.enum(["equals", "doesNotEqual", "equalsOneOf", "isSubmitted", "isSkipped"]),
});
const ZMultipleChoiceMultiCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceMulti),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.includesAllOf,
ZSurveyLogicCondition.Enum.includesOneOf,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"includesAllOf",
"includesOneOf",
"isSubmitted",
"isSkipped",
]),
});
const ZPictureSelectionCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.PictureSelection),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.includesAllOf,
ZSurveyLogicCondition.Enum.includesOneOf,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"includesAllOf",
"includesOneOf",
"isSubmitted",
"isSkipped",
]),
});
const ZRatingCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Rating),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.isGreaterThan,
ZSurveyLogicCondition.Enum.isLessThan,
ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
ZSurveyLogicCondition.Enum.isLessThanOrEqual,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"isGreaterThan",
"isLessThan",
"isGreaterThanOrEqual",
"isLessThanOrEqual",
"isSubmitted",
"isSkipped",
]),
matchValue: z.number().optional(),
});
@@ -182,68 +177,57 @@ const ZRatingCondition = ZConditionQuestionBase.extend({
const ZNPSCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.NPS),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.isGreaterThan,
ZSurveyLogicCondition.Enum.isLessThan,
ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
ZSurveyLogicCondition.Enum.isLessThanOrEqual,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
"equals",
"doesNotEqual",
"isGreaterThan",
"isLessThan",
"isGreaterThanOrEqual",
"isLessThanOrEqual",
"isSubmitted",
"isSkipped",
]),
matchValue: z.number().optional(),
});
const ZCTACondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.CTA),
conditionOperator: z.enum([ZSurveyLogicCondition.Enum.isClicked, ZSurveyLogicCondition.Enum.isSkipped]),
conditionOperator: z.enum(["isClicked", "isSkipped"]),
matchValue: z.undefined(),
});
const ZConsentCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Consent),
conditionOperator: z.enum([ZSurveyLogicCondition.Enum.isAccepted, ZSurveyLogicCondition.Enum.isSkipped]),
conditionOperator: z.enum(["isAccepted", "isSkipped"]),
matchValue: z.undefined(),
});
const ZDateCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Date),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.isBefore,
ZSurveyLogicCondition.Enum.isAfter,
ZSurveyLogicCondition.Enum.isSubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
]),
conditionOperator: z.enum(["equals", "doesNotEqual", "isBefore", "isAfter", "isSubmitted", "isSkipped"]),
matchValue: z.string().optional(),
});
const ZFileUploadCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.FileUpload),
conditionOperator: z.enum([ZSurveyLogicCondition.Enum.isSubmitted, ZSurveyLogicCondition.Enum.isSkipped]),
conditionOperator: z.enum(["isSubmitted", "isSkipped"]),
matchValue: z.undefined(),
});
const ZCalCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Cal),
conditionOperator: z.enum([ZSurveyLogicCondition.Enum.isBooked, ZSurveyLogicCondition.Enum.isSkipped]),
conditionOperator: z.enum(["isBooked", "isSkipped"]),
matchValue: z.undefined(),
});
const ZMatrixCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Matrix),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.isPartiallySubmitted,
ZSurveyLogicCondition.Enum.isCompletelySubmitted,
ZSurveyLogicCondition.Enum.isSkipped,
]),
conditionOperator: z.enum(["isPartiallySubmitted", "isCompletelySubmitted", "isSkipped"]),
matchValue: z.undefined(),
});
const ZAddressCondition = ZConditionQuestionBase.extend({
questionType: z.literal(TSurveyQuestionTypeEnum.Address),
conditionOperator: z.enum([ZSurveyLogicCondition.Enum.isSubmitted, ZSurveyLogicCondition.Enum.isSkipped]),
conditionOperator: z.enum(["isSubmitted", "isSkipped"]),
matchValue: z.undefined(),
});
@@ -271,26 +255,26 @@ const ZConditionVariableBase = ZConditionBase.extend({
const ZConditionTextVariable = ZConditionVariableBase.extend({
variableType: z.literal("text"),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.contains,
ZSurveyLogicCondition.Enum.doesNotContain,
ZSurveyLogicCondition.Enum.startsWith,
ZSurveyLogicCondition.Enum.doesNotStartWith,
ZSurveyLogicCondition.Enum.endsWith,
ZSurveyLogicCondition.Enum.doesNotEndWith,
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
"startsWith",
"doesNotStartWith",
"endsWith",
"doesNotEndWith",
]),
});
const ZConditionNumberVariable = ZConditionVariableBase.extend({
variableType: z.literal("number"),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.isGreaterThan,
ZSurveyLogicCondition.Enum.isLessThan,
ZSurveyLogicCondition.Enum.isGreaterThanOrEqual,
ZSurveyLogicCondition.Enum.isLessThanOrEqual,
"equals",
"doesNotEqual",
"isGreaterThan",
"isLessThan",
"isGreaterThanOrEqual",
"isLessThanOrEqual",
]),
});
@@ -299,28 +283,28 @@ const ZConditionVariable = z.union([ZConditionTextVariable, ZConditionNumberVari
const ZConditionAttributeClass = ZConditionBase.extend({
type: z.literal("attributeClass"),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.contains,
ZSurveyLogicCondition.Enum.doesNotContain,
ZSurveyLogicCondition.Enum.startsWith,
ZSurveyLogicCondition.Enum.doesNotStartWith,
ZSurveyLogicCondition.Enum.endsWith,
ZSurveyLogicCondition.Enum.doesNotEndWith,
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
"startsWith",
"doesNotStartWith",
"endsWith",
"doesNotEndWith",
]),
});
const ZConditionHiddenField = ZConditionBase.extend({
type: z.literal("hiddenField"),
conditionOperator: z.enum([
ZSurveyLogicCondition.Enum.equals,
ZSurveyLogicCondition.Enum.doesNotEqual,
ZSurveyLogicCondition.Enum.contains,
ZSurveyLogicCondition.Enum.doesNotContain,
ZSurveyLogicCondition.Enum.startsWith,
ZSurveyLogicCondition.Enum.doesNotStartWith,
ZSurveyLogicCondition.Enum.endsWith,
ZSurveyLogicCondition.Enum.doesNotEndWith,
"equals",
"doesNotEqual",
"contains",
"doesNotContain",
"startsWith",
"doesNotStartWith",
"endsWith",
"doesNotEndWith",
]),
});
@@ -344,6 +328,7 @@ export const ZActionObjective = z.nativeEnum(TActionObjective);
const ZActionBase = z.object({
id: z.string().cuid2(),
objective: ZActionObjective,
target: z.string(),
});
export type TActionBase = z.infer<typeof ZActionBase>;
@@ -371,7 +356,7 @@ export const ZActionTextVariableCalculateOperator = z.nativeEnum(TActionTextVari
const ZActionTextVariableCalculate = ZActionCalculateBase.extend({
variableType: z.literal(TActionCalculateVariableType.Text),
operator: ZActionTextVariableCalculateOperator,
value: z.string(),
value: z.union([z.string(), ZMatchValueDynamic]),
});
export enum TActionNumberVariableCalculateOperator {
@@ -387,10 +372,12 @@ export const ZActionNumberVariableCalculateOperator = z.nativeEnum(TActionNumber
const ZActionNumberVariableCalculate = ZActionCalculateBase.extend({
variableType: z.literal(TActionCalculateVariableType.Number),
operator: ZActionNumberVariableCalculateOperator,
value: z.number(),
value: z.union([z.number(), ZMatchValueDynamic]),
});
const ZActionCalculate = z.union([ZActionTextVariableCalculate, ZActionNumberVariableCalculate]);
export const ZActionCalculate = z.union([ZActionTextVariableCalculate, ZActionNumberVariableCalculate]);
export type TActionCalculate = z.infer<typeof ZActionCalculate>;
const ZActionRequireAnswer = ZActionBase.extend({
objective: z.literal("requireAnswer"),

View File

@@ -18,6 +18,7 @@ export interface ComboboxOption {
icon?: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
label: string;
value: string;
meta?: Record<string, string>;
}
export interface ComboboxGroupedOption {
@@ -32,7 +33,7 @@ interface InputComboboxProps {
options?: ComboboxOption[];
groupedOptions?: ComboboxGroupedOption[];
selected?: string | string[] | null;
onChangeValue: (option: string | string[]) => void;
onChangeValue: (value: string | string[], option?: ComboboxOption) => void;
inputProps?: React.ComponentProps<typeof Input>;
withInput?: boolean;
comboboxSize?: "sm" | "lg";
@@ -56,12 +57,16 @@ export const InputCombobox = ({
comboboxClasses,
}: InputComboboxProps) => {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<ComboboxOption | ComboboxOption[] | null>(() => {
const [value, setValue] = React.useState<ComboboxOption | ComboboxOption[] | null>(null);
React.useEffect(() => {
const validOptions = options?.length ? options : groupedOptions?.flatMap((group) => group.options);
if (Array.isArray(selected)) {
return options?.filter((option) => selected.includes(option.value)) || null;
setValue(validOptions?.filter((option) => selected.includes(option.value)) || null);
} else {
setValue(validOptions?.find((option) => option.value === selected) || null);
}
return options?.find((option) => option.value === selected) || null;
});
}, [selected, options, groupedOptions]);
const handleSelect = (option: ComboboxOption) => {
if (allowMultiSelect) {
@@ -71,11 +76,11 @@ export const InputCombobox = ({
onChangeValue(newValue.map((item) => item.value));
setValue(newValue);
} else {
onChangeValue([option.value]);
onChangeValue([option.value], option);
setValue([option]);
}
} else {
onChangeValue(option.value);
onChangeValue(option.value, option);
setValue(option);
setOpen(false);
}
@@ -107,21 +112,21 @@ export const InputCombobox = ({
<>
{idx !== 0 && <span>,</span>}
<div className="flex items-center gap-2">
{item?.icon && <item.icon className="h-4 w-4 text-slate-300" />}
{item?.icon && <item.icon className="h-5 w-5 shrink-0 text-slate-400" />}
<span>{item?.label}</span>
</div>
</>
))
) : (
<div className="flex items-center gap-2">
{value?.icon && <value.icon className="h-4 w-4 text-slate-300" />}
{value?.icon && <value.icon className="h-5 w-5 shrink-0 text-slate-400" />}
<span>{value?.label}</span>
</div>
)}
</div>
)}
<ChevronDownIcon
className="text-slate-300"
className="shrink-0 text-slate-300"
height={comboboxSize === "sm" ? 20 : 16}
width={comboboxSize === "sm" ? 20 : 16}
/>
@@ -150,7 +155,7 @@ export const InputCombobox = ({
(!allowMultiSelect && typeof value === "string" && value === option.value)) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300" />
)}
{option.icon && <option.icon className="mr-2 h-4 w-4 shrink-0 text-slate-400" />}
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
{option.label}
</CommandItem>
))}
@@ -169,7 +174,7 @@ export const InputCombobox = ({
(!allowMultiSelect && typeof value === "string" && value === option.value)) && (
<CheckIcon className="mr-2 h-4 w-4 shrink-0 text-slate-300" />
)}
{option.icon && <option.icon className="mr-2 h-4 w-4 text-slate-300" />}
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
{option.label}
</CommandItem>
))}