mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 03:21:51 -05:00
component render fix based on new schema
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user