component render fix based on new schema

This commit is contained in:
Piyush Gupta
2024-08-25 23:16:19 +05:30
parent d876c495be
commit 71c3ac0e4e
10 changed files with 497 additions and 464 deletions

View File

@@ -1,8 +1,6 @@
import { AdvancedLogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditorActions";
import { AdvancedLogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedLogicEditorConditions";
import { createId } from "@paralleldrive/cuid2";
import { removeAction } from "@formbricks/lib/survey/logic/utils";
import { TAction, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
interface AdvancedLogicEditorProps {
@@ -24,40 +22,6 @@ export function AdvancedLogicEditor({
logicIdx,
userAttributes,
}: AdvancedLogicEditorProps) {
const handleActionsChange = (
operation: "delete" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: Partial<TAction>
) => {
const actionsClone = structuredClone(logicItem.actions);
let updatedActions: TSurveyAdvancedLogic["actions"] = actionsClone;
if (operation === "delete") {
updatedActions = removeAction(actionsClone, actionIdx);
} 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, {
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
@@ -71,7 +35,9 @@ export function AdvancedLogicEditor({
/>
<AdvancedLogicEditorActions
logicItem={logicItem}
handleActionsChange={handleActionsChange}
logicIdx={logicIdx}
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
userAttributes={userAttributes}
questionIdx={questionIdx}

View File

@@ -1,21 +1,20 @@
import {
actionObjectiveOptions,
getActionOpeartorOptions,
getActionTargetOptions,
getActionValueOptions,
getActionVariableOptions,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, CornerDownRightIcon, MoreVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { actionObjectiveOptions } from "@formbricks/lib/survey/logic/utils";
import {
TAction,
TActionCalculateVariableType,
TActionNumberVariableCalculateOperator,
TActionObjective,
TActionTextVariableCalculateOperator,
TDyanmicLogicField,
TActionVariableCalculateOperator,
TSurveyAdvancedLogic,
ZAction,
} from "@formbricks/types/surveys/logic";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,11 +26,9 @@ import { InputCombobox } from "@formbricks/ui/InputCombobox";
interface AdvancedLogicEditorActions {
localSurvey: TSurvey;
logicItem: TSurveyAdvancedLogic;
handleActionsChange: (
operation: "delete" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: Partial<TAction>
) => void;
logicIdx: number;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
userAttributes: string[];
questionIdx: number;
}
@@ -39,16 +36,78 @@ interface AdvancedLogicEditorActions {
export function AdvancedLogicEditorActions({
localSurvey,
logicItem,
handleActionsChange,
logicIdx,
question,
updateQuestion,
userAttributes,
questionIdx,
}: AdvancedLogicEditorActions) {
const actions = logicItem.actions;
const updateAction = (actionIdx: number, updatedAction: Partial<TAction>) => {
handleActionsChange("update", actionIdx, updatedAction);
const handleActionsChange = (
operation: "delete" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: TAction
) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
const logicItem = advancedLogicCopy[logicIdx];
const actionsClone = logicItem.actions;
if (operation === "delete") {
actionsClone.splice(actionIdx, 1);
} else if (operation === "addBelow") {
actionsClone.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" });
} else if (operation === "duplicate") {
actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() });
} else if (operation === "update") {
if (!action) return;
actionsClone[actionIdx] = action;
}
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const getUpdatedActionBody = (action, update) => {
switch (update.objective) {
case "calculate":
return {
...action,
...update,
objective: "calculate", // Ensure objective remains 'calculate'
variableId: "",
operator: "assign",
value: update.value ? { ...action.value, ...update.value } : { type: "static", value: "" },
};
case "requireAnswer":
return {
...action,
...update,
objective: "requireAnswer", // Ensure objective remains 'requireAnswer'
target: "",
};
case "jumpToQuestion":
return {
...action,
...update,
objective: "jumpToQuestion", // Ensure objective remains 'jumpToQuestion'
target: "",
};
}
};
function updateAction(actionIdx: number, update: Partial<TAction>) {
const action = actions[actionIdx];
const actionBody = getUpdatedActionBody(action, update);
const parsedActionBodyResult = ZAction.safeParse(actionBody);
if (!parsedActionBodyResult.success) {
console.error("Failed to update action", parsedActionBodyResult.error.errors);
return;
}
handleActionsChange("update", actionIdx, parsedActionBodyResult.data);
}
console.log("actions", actions);
return (
<div className="">
@@ -67,9 +126,6 @@ export function AdvancedLogicEditorActions({
onChangeValue={(val: TActionObjective) => {
updateAction(idx, {
objective: val,
target: "",
operator: undefined,
variableType: undefined,
});
}}
comboboxClasses="max-w-[200px]"
@@ -82,11 +138,10 @@ export function AdvancedLogicEditorActions({
? getActionVariableOptions(localSurvey)
: getActionTargetOptions(localSurvey, questionIdx)
}
selected={action.target}
onChangeValue={(val: string, option) => {
selected={action.objective === "calculate" ? action.variableId : action.target}
onChangeValue={(val: string) => {
updateAction(idx, {
target: val,
variableType: option?.meta?.variableType as TActionCalculateVariableType,
...(action.objective === "calculate" ? { variableId: val } : { target: val }),
});
}}
comboboxClasses="grow min-w-[100px]"
@@ -96,11 +151,11 @@ export function AdvancedLogicEditorActions({
<InputCombobox
key="attribute"
showSearch={false}
options={getActionOpeartorOptions(action.variableType)}
options={getActionOpeartorOptions(
localSurvey.variables.find((v) => v.id === action.variableId)?.type
)}
selected={action.operator}
onChangeValue={(
val: TActionNumberVariableCalculateOperator | TActionTextVariableCalculateOperator
) => {
onChangeValue={(val: TActionVariableCalculateOperator) => {
updateAction(idx, {
operator: val,
});
@@ -112,30 +167,29 @@ export function AdvancedLogicEditorActions({
withInput={true}
inputProps={{
placeholder: "Value",
value: typeof action.value !== "object" ? action.value : "",
type: action.variableType,
value: action.value?.value ?? "",
type: localSurvey.variables.find((v) => v.id === action.variableId)?.type || "text",
onChange: (e) => {
let val: string | number = e.target.value;
if (action.variableType === "number") {
const variable = localSurvey.variables.find((v) => v.id === action.variableId);
if (variable?.type === "number") {
val = Number(val);
updateAction(idx, {
value: val,
});
} else if (action.variableType === "text") {
updateAction(idx, {
value: val,
});
}
updateAction(idx, {
value: {
type: "static",
value: val,
},
});
},
}}
groupedOptions={getActionValueOptions(localSurvey, questionIdx, userAttributes)}
onChangeValue={(val: string, option) => {
onChangeValue={(val: string) => {
updateAction(idx, {
value: {
id: val,
fieldType: option?.meta?.fieldType as TDyanmicLogicField,
type: "dynamic",
type: "static",
value: val,
},
});
}}

View File

@@ -6,9 +6,16 @@ import {
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, TSurveyLogicCondition } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
addConditionBelow,
createGroupFromResource,
duplicateCondition,
isConditionsGroup,
removeCondition,
toggleGroupConnector,
} from "@formbricks/lib/survey/logic/utils";
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
import { TSurvey, TSurveyLogicCondition, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,13 +25,14 @@ import {
import { InputCombobox } from "@formbricks/ui/InputCombobox";
interface AdvancedLogicEditorConditions {
conditions: TSurveyAdvancedLogic["conditions"];
conditions: TConditionGroup;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
localSurvey: TSurvey;
questionIdx: number;
logicIdx: number;
userAttributes: string[];
depth?: number;
}
export function AdvancedLogicEditorConditions({
@@ -35,33 +43,22 @@ export function AdvancedLogicEditorConditions({
questionIdx,
updateQuestion,
userAttributes,
depth = 0,
}: AdvancedLogicEditorConditions) {
const handleAddConditionBelow = (resourceId: string, condition: Partial<TConditionBase>) => {
const handleAddConditionBelow = (resourceId: string, condition: TSingleCondition) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({
action: "addConditionBelow",
advancedLogicCopy,
logicIdx,
resourceId,
condition,
});
const logicItem = advancedLogicCopy[logicIdx];
addConditionBelow(logicItem.conditions, resourceId, condition);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleConnectorChange = (resourceId: string, connector: TConditionBase["connector"]) => {
if (!connector) return;
const handleConnectorChange = (groupId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({
action: "toggleConnector",
advancedLogicCopy,
logicIdx,
resourceId,
});
const logicItem = advancedLogicCopy[logicIdx];
toggleGroupConnector(logicItem.conditions, groupId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -70,13 +67,8 @@ export function AdvancedLogicEditorConditions({
const handleRemoveCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({
action: "removeCondition",
advancedLogicCopy,
logicIdx,
resourceId,
});
const logicItem = advancedLogicCopy[logicIdx];
removeCondition(logicItem.conditions, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -85,8 +77,8 @@ export function AdvancedLogicEditorConditions({
const handleDuplicateCondition = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({ action: "duplicateCondition", advancedLogicCopy, logicIdx, resourceId });
const logicItem = advancedLogicCopy[logicIdx];
duplicateCondition(logicItem.conditions, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -95,29 +87,25 @@ export function AdvancedLogicEditorConditions({
const handleCreateGroup = (resourceId: string) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
const logicItem = advancedLogicCopy[logicIdx];
performOperationsOnConditions({
action: "createGroup",
advancedLogicCopy,
logicIdx,
resourceId,
});
createGroupFromResource(logicItem.conditions, resourceId);
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
});
};
const handleUpdateCondition = (resourceId: string, updateConditionBody: Partial<TConditionBase>) => {
const handleUpdateCondition = (resourceId: string, updateConditionBody: Partial<TSingleCondition>) => {
const advancedLogicCopy = structuredClone(question.advancedLogic) || [];
performOperationsOnConditions({
action: "updateCondition",
advancedLogicCopy,
logicIdx,
resourceId,
conditionBody: updateConditionBody,
});
// performOperationsOnConditions({
// action: "updateCondition",
// advancedLogicCopy,
// logicIdx,
// resourceId,
// conditionBody: updateConditionBody,
// });
updateQuestion(questionIdx, {
advancedLogic: advancedLogicCopy,
@@ -126,167 +114,185 @@ export function AdvancedLogicEditorConditions({
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
conditions={condition.conditions}
key={id}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
userAttributes={userAttributes}
/>
</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>
const renderCondition = (
condition: TSingleCondition | TConditionGroup,
index: number,
parentConditionGroup: TConditionGroup
) => {
const connector = parentConditionGroup.connector;
if (isConditionsGroup(condition)) {
return (
<div key={condition.id} className="flex items-start justify-between gap-4">
{index === 0 ? (
<div className="w-14 text-sm">When</div>
) : (
<div
className={cn("w-14 text-sm", { "cursor-pointer underline": index === 1 })}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);
}}>
{connector}
</div>
);
}
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">
<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>
<InputCombobox
key="conditionValue"
showSearch={false}
groupedOptions={conditionValueOptions}
selected={condition.conditionValue}
onChangeValue={(val: string, option) => {
handleUpdateCondition(id, {
conditionValue: val,
...option?.meta,
});
}}
comboboxClasses="grow"
)}
<div className="mt-2 rounded-lg border border-slate-200 bg-slate-100 p-4">
<AdvancedLogicEditorConditions
conditions={condition}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
userAttributes={userAttributes}
depth={depth + 1}
/>
<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={() => {}}
/>
)}
</div>
<div className="mt-2">
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleAddConditionBelow(id, {
handleAddConditionBelow(condition.id, {
id: createId(),
connector: "and",
conditionValue: localSurvey.questions[questionIdx].id,
type: "question",
questionType: localSurvey.questions[questionIdx].type,
leftOperand: {
id: localSurvey.questions[questionIdx].id,
type: "question",
},
operator: "equals",
});
}}>
<PlusIcon className="h-4 w-4" />
Add condition below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={conditions.length === 1}
onClick={() => {
handleRemoveCondition(id);
}}>
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.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>
);
}
const conditionValueOptions = getConditionValueOptions(localSurvey, questionIdx, userAttributes);
const conditionOperatorOptions = getConditionOperatorOptions(condition, localSurvey);
const { show, options } = getMatchValueProps(localSurvey, condition, questionIdx, userAttributes);
return (
<div key={condition.id} className="mt-2 flex items-center justify-between gap-4">
{index === 0 ? (
<div className="w-14 text-sm">When</div>
) : (
<div
className={cn("w-14 text-sm", { "cursor-pointer underline": index === 1 })}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);
}}>
{connector}
</div>
)}
<InputCombobox
key="conditionValue"
showSearch={false}
groupedOptions={conditionValueOptions}
selected={condition.leftOperand.id}
onChangeValue={(val: string, option) => {
handleUpdateCondition(condition.id, {
leftOperand: {
id: val,
type: option?.meta?.type,
},
});
}}
comboboxClasses="grow"
/>
<InputCombobox
key="conditionOperator"
showSearch={false}
options={conditionOperatorOptions}
selected={condition.operator}
onChangeValue={(val: TSurveyLogicCondition) => {
handleUpdateCondition(condition.id, {
operator: val,
});
}}
comboboxClasses="grow"
/>
{show && options.length > 0 && (
<InputCombobox
withInput
key="conditionMatchValue"
showSearch={false}
groupedOptions={options}
comboboxSize="sm"
selected={condition.rightOperand?.value}
onChangeValue={(val) => {
handleUpdateCondition(condition.id, {
rightOperand: {
...condition.rightOperand,
value: val,
},
});
}}
/>
)}
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleAddConditionBelow(condition.id, {
id: createId(),
leftOperand: {
id: localSurvey.questions[questionIdx].id,
type: "question",
},
operator: "equals",
});
}}>
<PlusIcon className="h-4 w-4" />
Add condition below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.id)}>
<Trash2Icon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => handleDuplicateCondition(condition.id)}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => handleCreateGroup(condition.id)}>
<WorkflowIcon className="h-4 w-4" />
Create group
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
return (
<div className="flex flex-col gap-4">
{conditions.conditions.map((condition, index) => renderCondition(condition, index, conditions))}
</div>
);
}

View File

@@ -19,17 +19,6 @@ interface ConditionalLogicProps {
userAttributes: string[];
}
const initialLogicState = {
id: createId(),
conditions: [
{
id: createId(),
connector: null,
},
],
actions: [{ objective: "" }],
};
export function ConditionalLogic({
attributeClasses,
localSurvey,
@@ -43,19 +32,36 @@ export function ConditionalLogic({
}, [localSurvey, attributeClasses]);
const addLogic = () => {
const condition: TSurveyAdvancedLogic = {
const initialCondition: TSurveyAdvancedLogic = {
id: createId(),
conditions: [
{
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
type: "question",
id: localSurvey.questions[0].id,
},
],
},
],
operator: "isSkipped",
},
{
id: createId(),
connector: "or",
conditions: [
{
id: createId(),
leftOperand: {
type: "question",
id: localSurvey.questions[0].id,
},
operator: "isSkipped",
},
],
},
],
},
actions: [
{
id: createId(),
@@ -66,7 +72,7 @@ export function ConditionalLogic({
};
updateQuestion(questionIdx, {
advancedLogic: [...(question?.advancedLogic || []), condition],
advancedLogic: [...(question?.advancedLogic || []), initialCondition],
});
};

View File

@@ -1,4 +1,5 @@
import { TSurveyQuestionTypeEnum, ZSurveyLogicCondition } from "@formbricks/types/surveys/logic";
import { ZSurveyLogicCondition } from "@formbricks/types/surveys/logic";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
export const ruleEngine = {
question: {

View File

@@ -20,13 +20,11 @@ import {
} from "lucide-react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import {
TActionCalculateVariableType,
TActionNumberVariableCalculateOperator,
TActionTextVariableCalculateOperator,
TCondition,
TSurveyQuestionTypeEnum,
TActionObjective,
TActionVariableCalculateOperator,
TSingleCondition,
} from "@formbricks/types/surveys/logic";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyQuestionTypeEnum, TSurveyVariable } from "@formbricks/types/surveys/types";
import { ComboboxGroupedOption, ComboboxOption } from "@formbricks/ui/InputCombobox";
// formats the text to highlight specific parts of the text with slashes
@@ -83,8 +81,6 @@ export const getConditionValueOptions = (
value: question.id,
meta: {
type: "question",
questionType: question.type,
inputType: question.type === "openText" ? question.inputType : "",
},
};
});
@@ -96,7 +92,6 @@ export const getConditionValueOptions = (
value: variable.id,
meta: {
type: "variable",
variableType: variable.type,
},
};
});
@@ -158,26 +153,40 @@ export const getConditionValueOptions = (
return groupedOptions;
};
export const getConditionOperatorOptions = (condition: TCondition): ComboboxOption[] => {
if (condition.type === "attributeClass") {
export const actionObjectiveOptions: ComboboxOption<TActionObjective>[] = [
{ label: "Calculate", value: "calculate" },
{ label: "Require Answer", value: "requireAnswer" },
{ label: "Jump to question", value: "jumpToQuestion" },
];
export const getConditionOperatorOptions = (
condition: TSingleCondition,
localSurvey: TSurvey
): ComboboxOption[] => {
if (condition.leftOperand.type === "userAttribute") {
return ruleEngine.userAttribute.options;
} else if (condition.type === "variable") {
return ruleEngine.variable[condition.variableType].options;
} else if (condition.type === "hiddenField") {
} else if (condition.leftOperand.type === "variable") {
const variables = localSurvey.variables || [];
const variableType =
variables.find((variable) => variable.id === condition.leftOperand.id)?.type || "text";
return ruleEngine.variable[variableType].options;
} else if (condition.leftOperand.type === "hiddenField") {
return ruleEngine.hiddenField.options;
} else if (condition.type === "question") {
if (condition.questionType === "openText") {
const inputType = condition.inputType === "number" ? "number" : "text";
} else if (condition.leftOperand.type === "question") {
const questions = localSurvey.questions || [];
const question = questions.find((question) => question.id === condition.leftOperand.id) || questions[0];
if (question.type === "openText") {
const inputType = question.inputType === "number" ? "number" : "text";
return ruleEngine.question.openText[inputType].options;
}
return ruleEngine.question[condition.questionType].options;
return ruleEngine.question[question.type].options;
}
return [];
};
export const getMatchValueProps = (
localSurvey: TSurvey,
condition: TCondition,
condition: TSingleCondition,
questionIdx: number,
userAttributes: string[]
): { show: boolean; options: ComboboxGroupedOption[] } => {
@@ -190,7 +199,7 @@ export const getMatchValueProps = (
"isPartiallySubmitted",
"isSkipped",
"isSubmitted",
].includes(condition.conditionOperator)
].includes(condition.operator)
) {
return { show: false, options: [] };
}
@@ -243,18 +252,18 @@ export const getMatchValueProps = (
});
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") {
if (condition.leftOperand.type === "hiddenField") {
hiddenFieldsOptions = hiddenFieldsOptions?.filter((field) => field.value !== condition.leftOperand.id);
} else if (condition.leftOperand.type === "variable") {
variableOptions = variableOptions?.filter((variable) => variable.value !== condition.leftOperand.id);
} else if (condition.leftOperand.type === "userAttribute") {
userAttributesOptions = userAttributesOptions?.filter(
(attribute) => attribute.value !== condition.conditionValue
(attribute) => attribute.value !== condition.leftOperand.id
);
} else if (condition.type === "question") {
questionOptions = questionOptions?.filter((question) => question.value !== condition.conditionValue);
} else if (condition.leftOperand.type === "question") {
questionOptions = questionOptions?.filter((question) => question.value !== condition.leftOperand.id);
const question = localSurvey.questions.find((question) => question.id === condition.conditionValue);
const question = localSurvey.questions.find((question) => question.id === condition.leftOperand.id);
let choices: ComboboxOption[] = [];
if (
@@ -361,39 +370,41 @@ export const getActionVariableOptions = (localSurvey: TSurvey): ComboboxOption[]
});
};
export const getActionOpeartorOptions = (variableType: TActionCalculateVariableType): ComboboxOption[] => {
if (variableType === TActionCalculateVariableType.Number) {
export const getActionOpeartorOptions = (
variableType?: TSurveyVariable["type"]
): ComboboxOption<TActionVariableCalculateOperator>[] => {
if (variableType === "number") {
return [
{
label: "Add +",
value: TActionNumberVariableCalculateOperator.Add,
value: "add",
},
{
label: "Subtract -",
value: TActionNumberVariableCalculateOperator.Subtract,
value: "subtract",
},
{
label: "Multiply *",
value: TActionNumberVariableCalculateOperator.Multiply,
value: "multiply",
},
{
label: "Divide /",
value: TActionNumberVariableCalculateOperator.Divide,
value: "divide",
},
{
label: "Assign =",
value: TActionNumberVariableCalculateOperator.Assign,
value: "assign",
},
];
} else if (variableType === TActionCalculateVariableType.Text) {
} else if (variableType === "text") {
return [
{
label: "Assign =",
value: TActionTextVariableCalculateOperator.Assign,
value: "assign",
},
{
label: "Concat +",
value: TActionTextVariableCalculateOperator.Concat,
value: "concat",
},
];
}

View File

@@ -1,164 +1,133 @@
import { createId } from "@paralleldrive/cuid2";
import { TActionObjective, TConditionBase, TSurveyAdvancedLogic } from "@formbricks/types/surveys/logic";
import { TConditionGroup, TSingleCondition } from "@formbricks/types/surveys/logic";
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];
type TCondition = TSingleCondition | TConditionGroup;
if (action === "addConditionBelow") {
if (!condition) return;
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);
} else if (action === "updateCondition") {
if (!conditionBody) return;
updateCondition(logicItem.conditions, resourceId, conditionBody);
}
advancedLogicCopy[logicIdx] = {
...logicItem,
conditions: logicItem.conditions,
};
};
export const removeAction = (actions: TSurveyAdvancedLogic["actions"], idx: number) => {
return actions.slice(0, idx).concat(actions.slice(idx + 1));
export const isConditionsGroup = (condition: TCondition): condition is TConditionGroup => {
return (condition as TConditionGroup).connector !== undefined;
};
export const addConditionBelow = (
group: TSurveyAdvancedLogic["conditions"],
group: TConditionGroup,
resourceId: string,
condition: TConditionBase
condition: TSingleCondition
) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[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);
if (item.connector) {
if (item.id === resourceId) {
group.conditions.splice(i + 1, 0, condition);
break;
} else {
if (type === "group") {
addConditionBelow(group[i].conditions, resourceId, condition);
}
addConditionBelow(item, resourceId, condition);
}
} else {
if (item.id === resourceId) {
group.conditions.splice(i + 1, 0, condition);
break;
}
}
}
};
export const toggleGroupConnector = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
export const toggleGroupConnector = (group: TConditionGroup, resourceId: string) => {
if (group.id === resourceId) {
group.connector = group.connector === "and" ? "or" : "and";
return;
}
if (id === resourceId) {
console.log("madarchod", group[i].connector);
group[i].connector = group[i].connector === "and" ? "or" : "and";
return;
for (const condition of group.conditions) {
if (condition.connector) {
toggleGroupConnector(condition, resourceId);
}
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];
export const removeCondition = (group: TConditionGroup, resourceId: string) => {
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (id === resourceId) {
if (i === 0) group[i + 1].connector = null;
group.splice(i, 1);
if (item.id === resourceId) {
group.conditions.splice(i, 1);
return;
}
if (type === "group") removeCondition(group[i].conditions, resourceId);
if (item.connector) {
removeCondition(item, resourceId);
}
}
deleteEmptyGroups(group);
};
export const duplicateCondition = (group: TSurveyAdvancedLogic["conditions"], resourceId: string) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
export const duplicateCondition = (group: TConditionGroup, resourceId: string) => {
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (id === resourceId) {
group.splice(i + 1, 0, {
...group[i],
if (item.id === resourceId) {
const newItem: TCondition = {
...item,
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 }],
};
group.conditions.splice(i + 1, 0, newItem);
return;
}
if (type === "group") createGroupFromResource(group[i].conditions, resourceId);
if (item.connector) {
duplicateCondition(item, resourceId);
}
}
};
export const deleteEmptyGroups = (group: TConditionGroup) => {
for (let i = 0; i < group.conditions.length; i++) {
const resource = group.conditions[i];
if (isConditionsGroup(resource) && resource.conditions.length === 0) {
group.conditions.splice(i, 1);
} else if (isConditionsGroup(resource)) {
deleteEmptyGroups(resource);
}
}
};
export const createGroupFromResource = (group: TConditionGroup, resourceId: string) => {
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (item.id === resourceId) {
const newGroup: TConditionGroup = {
id: createId(),
connector: "and",
conditions: [item],
};
group.conditions[i] = newGroup;
group.connector = "and";
return;
}
if (item.connector) {
createGroupFromResource(item, resourceId);
}
}
};
export const updateCondition = (
group: TSurveyAdvancedLogic["conditions"],
group: TConditionGroup,
resourceId: string,
condition: Partial<TConditionBase>
condition: Partial<TSingleCondition>
) => {
for (let i = 0; i < group.length; i++) {
const { type, id } = group[i];
for (let i = 0; i < group.conditions.length; i++) {
const item = group.conditions[i];
if (id === resourceId) {
group[i] = { ...group[i], ...condition };
if (item.id === resourceId && !("connector" in item)) {
group.conditions[i] = { ...item, ...condition } as TSingleCondition;
return;
}
if (type === "group") updateCondition(group[i].conditions, resourceId, condition);
if (item.connector) {
updateCondition(item, resourceId, condition);
}
}
};
export const actionObjectiveOptions: { label: string; value: TActionObjective }[] = [
{ label: "Calculate", value: TActionObjective.Calculate },
{ label: "Require Answer", value: TActionObjective.RequireAnswer },
{ label: "Jump to question", value: TActionObjective.JumpToQuestion },
];

View File

@@ -323,6 +323,16 @@ export const ZActionNumberVariableCalculateOperator = z.enum([
"divide",
"assign",
]);
export const ZActionVariableCalculateOperator = z.union([
ZActionTextVariableCalculateOperator,
ZActionNumberVariableCalculateOperator,
]);
export type TDyanmicLogicField = z.infer<typeof ZDyanmicLogicField>;
export type TActionObjective = z.infer<typeof ZActionObjective>;
export type TActionTextVariableCalculateOperator = z.infer<typeof ZActionTextVariableCalculateOperator>;
export type TActionNumberVariableCalculateOperator = z.infer<typeof ZActionNumberVariableCalculateOperator>;
export type TActionVariableCalculateOperator = z.infer<typeof ZActionVariableCalculateOperator>;
// Conditions
const ZLeftOperandBase = z.object({
@@ -378,26 +388,32 @@ export const ZRightOperand = z.union([
export type TRightOperand = z.infer<typeof ZRightOperand>;
export const ZSurveyAdvancedLogicCondition = z.object({
id: z.string().cuid2(),
leftOperand: ZLeftOperand,
operator: ZSurveyLogicCondition,
rightOperand: ZRightOperand.optional(),
});
export const ZSingleCondition = z
.object({
id: z.string().cuid2(),
leftOperand: ZLeftOperand,
operator: ZSurveyLogicCondition,
rightOperand: ZRightOperand.optional(),
})
.and(
z.object({
connector: z.undefined(),
})
);
export type TSurveyAdvancedLogicCondition = z.infer<typeof ZSurveyAdvancedLogicCondition>;
export type TSingleCondition = z.infer<typeof ZSingleCondition>;
interface TSurveyAdvancedLogicConditions {
export interface TConditionGroup {
id: string;
connector: "and" | "or";
conditions: (TSurveyAdvancedLogicCondition | TSurveyAdvancedLogicConditions)[];
connector: "and" | "or" | null;
conditions: (TSingleCondition | TConditionGroup)[];
}
const ZSurveyAdvancedLogicConditions: z.ZodType<TSurveyAdvancedLogicConditions> = z.lazy(() =>
const ZConditionGroup: z.ZodType<TConditionGroup> = z.lazy(() =>
z.object({
id: z.string().cuid2(),
connector: z.enum(["and", "or"]),
conditions: z.array(z.union([ZSurveyAdvancedLogicCondition, ZSurveyAdvancedLogicConditions])),
connector: z.enum(["and", "or"]).nullable(),
conditions: z.array(z.union([ZSingleCondition, ZConditionGroup])),
})
);
@@ -412,7 +428,7 @@ export type TActionBase = z.infer<typeof ZActionBase>;
const ZActionCalculate = ZActionBase.extend({
objective: z.literal("calculate"),
variableId: z.string(),
operator: z.union([ZActionTextVariableCalculateOperator, ZActionNumberVariableCalculateOperator]),
operator: ZActionVariableCalculateOperator,
value: z.object({
type: z.union([z.literal("static"), ZDyanmicLogicField]),
value: z.union([z.string(), z.number()]),
@@ -425,20 +441,23 @@ const ZActionRequireAnswer = ZActionBase.extend({
objective: z.literal("requireAnswer"),
target: z.string(),
});
export type TActionRequireAnswer = z.infer<typeof ZActionRequireAnswer>;
const ZActionJumpToQuestion = ZActionBase.extend({
objective: z.literal("jumpToQuestion"),
target: z.string(),
});
const ZAction = z.union([ZActionCalculate, ZActionRequireAnswer, ZActionJumpToQuestion]);
export type TActionJumpToQuestion = z.infer<typeof ZActionJumpToQuestion>;
export const ZAction = z.union([ZActionCalculate, ZActionRequireAnswer, ZActionJumpToQuestion]);
export type TAction = z.infer<typeof ZAction>;
const ZSurveyAdvancedLogicActions = z.array(ZAction);
export const ZSurveyAdvancedLogic = z.object({
id: z.string().cuid2(),
conditions: z.array(ZSurveyAdvancedLogicConditions),
conditions: ZConditionGroup,
actions: ZSurveyAdvancedLogicActions,
});

View File

@@ -1,7 +1,7 @@
import { z } from "zod";
import { ZActionClass, ZActionClassNoCodeConfig } from "../action-classes";
import { ZAttributes } from "../attributes";
import { ZAllowedFileExtension, ZColor, ZPlacement , ZId } from "../common";
import { ZAllowedFileExtension, ZColor, ZId, ZPlacement } from "../common";
import { ZLanguage } from "../product";
import { ZSegment } from "../segment";
import { ZBaseStyling } from "../styling";

View File

@@ -14,10 +14,10 @@ import {
import { Input } from "../Input";
import { Popover, PopoverContent, PopoverTrigger } from "../Popover";
export interface ComboboxOption {
export interface ComboboxOption<T = string> {
icon?: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
label: string;
value: string;
value: T;
meta?: Record<string, string>;
}
@@ -27,13 +27,13 @@ export interface ComboboxGroupedOption {
options: ComboboxOption[];
}
interface InputComboboxProps {
interface InputComboboxProps<T> {
showSearch?: boolean;
searchPlaceholder?: string;
options?: ComboboxOption[];
options?: ComboboxOption<T>[];
groupedOptions?: ComboboxGroupedOption[];
selected?: string | string[] | null;
onChangeValue: (value: string | string[], option?: ComboboxOption) => void;
selected?: string | number | string[] | null;
onChangeValue: (value: T | T[], option?: ComboboxOption) => void;
inputProps?: React.ComponentProps<typeof Input>;
withInput?: boolean;
comboboxSize?: "sm" | "lg";
@@ -55,7 +55,7 @@ export const InputCombobox = ({
allowMultiSelect = false,
showCheckIcon = false,
comboboxClasses,
}: InputComboboxProps) => {
}: InputComboboxProps<string>) => {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<ComboboxOption | ComboboxOption[] | null>(null);
@@ -69,6 +69,7 @@ export const InputCombobox = ({
}, [selected, options, groupedOptions]);
const handleSelect = (option: ComboboxOption) => {
console.log("option", option);
if (allowMultiSelect) {
if (Array.isArray(value)) {
const doesExist = value.find((item) => item.value === option.value);