Skip to content

feat: Advanced logic editor#3020

Merged
pandeymangg merged 88 commits intomainfrom
feat-advanced-logic-editor
Sep 30, 2024
Merged

feat: Advanced logic editor#3020
pandeymangg merged 88 commits intomainfrom
feat-advanced-logic-editor

Conversation

@gupta-piyush19
Copy link
Contributor

@gupta-piyush19 gupta-piyush19 commented Aug 19, 2024

What does this PR do?

This pull request includes significant updates to the logic editor documentation, image format changes, navigation updates, survey template refactoring, and component imports. The most important changes include adding a new Logic Editor documentation page, converting image formats from PNG to WEBP, updating the navigation to include the new Logic Editor, refactoring survey templates to use functions, and updating the AdvancedSettings component to use ConditionalLogic.

Documentation Updates:

Image Format Changes:

Navigation Updates:

Survey Template Refactoring:

Component Import Updates:

Fixes
#3059
https://github.com/formbricks/internal/issues/273
https://github.com/formbricks/internal/issues/312

and also #2691

@vercel
Copy link

vercel bot commented Aug 19, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
formbricks-cloud ⬜️ Ignored (Inspect) Visit Preview Sep 30, 2024 9:58am
formbricks-docs ⬜️ Ignored (Inspect) Visit Preview Sep 30, 2024 9:58am

@github-actions
Copy link
Contributor

github-actions bot commented Aug 19, 2024

Thank you for following the naming conventions for pull request titles! 🙏

@jobenjada
Copy link
Member

jobenjada commented Aug 29, 2024

hey Piyush, I'll pre-review this to save us a feedback loop 🤓

It's looking great already! Here's what I noticed?

  1. Let's move this inside of the last block of Conditional Logic. Some users created logic for all options because they didn't see it. If it sits inside the gray box, it should be clearer. Also, make sure the font size and color is the same like the "When" and "Then" texts in the gray container:
image
  1. Text size: The rest of the surey editor is sm so please change all of the texts to sm within the logic editor
image

2.b. Icon size and color: These icons can be a bit smaller and darker for better usability
image

  1. Keep spacing between input fields consistent: In the conditions its too wide, in the actions too narrow. Maybe gap-x-2 should work
image

also gap here is too wide and inconsistent with gap between Condition and Action
image

I think you can get rid of mt-2 here
image

  1. hover:cursor-pointer
image
  1. Remove the outer gray container: It's clear enough and gives us some space
image
  1. Replace Emoji with lucide file-digit
image

here as well:
image

  1. Tweak Combobox UI: border-r not rounded if item selected
image

and there is a double border separating the input from the dropdown
image

  1. Spacing: Remove top padding
image

8.b. This icon needs more space on the right:
image

  1. Logic block: Since they are executed in sync, we need to allow resorting them. Replace delete icon with context menu for logic block with: Duplicate, Move Up, Move Down, and Delete. Add the icons we use for this functionality on Question Cards
image

10 Update Delete icon to match rest of survey editor:
image

vs

image
  1. I think this needs to get filtered out? Not sure if that makes sense to select the same variable? At least it wont let me select it:
image

That's it in terms of UI / UX. I'll do a proper test when you've marked the PR ready for review 🤓

Thanks, great work!!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (2)
packages/ui/components/InputCombobox/stories.ts (1)

37-49: LGTM: Default story covers key features.

The Default story effectively demonstrates the InputCombobox's main features, including search, icons, and clearable selection.

Consider adding a comment above the onChangeValue prop to indicate that the console.log is for demonstration purposes only and should not be used in production code.

packages/ui/components/InputCombobox/index.tsx (1)

48-63: Avoid defaulting the id prop to a static value

The id prop is marked as required in InputComboboxProps, but a default value of "temp" is provided (line 49). This could lead to multiple instances of the component sharing the same id, which may cause accessibility issues. It's better to enforce providing a unique id for each instance.

Consider removing the default value for id:

 export const InputCombobox = ({
-  id = "temp",
+  id,
   showSearch = true,
   searchPlaceholder = "Search...",
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 0aaca1e and 5acbd5d.

📒 Files selected for processing (5)
  • apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx (1 hunks)
  • apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx (1 hunks)
  • apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx (1 hunks)
  • packages/ui/components/InputCombobox/index.tsx (1 hunks)
  • packages/ui/components/InputCombobox/stories.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions.tsx
  • apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions.tsx
🧰 Additional context used
🪛 Biome
apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx

[error] 45-45: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

packages/ui/components/InputCombobox/index.tsx

[error] 176-176: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


[error] 297-297: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

🔇 Additional comments (7)
packages/ui/components/InputCombobox/stories.ts (6)

1-29: LGTM: Imports and metadata are well-structured.

The imports and metadata for the InputCombobox stories are appropriately defined. The metadata provides a clear and comprehensive description of the component's features, which is valuable for documentation.


31-35: LGTM: Common options are well-defined.

The commonOptions array is well-structured and promotes code reusability across multiple stories. Each option includes a label, value, and icon, which is consistent and informative.


51-88: LGTM: WithInput, GroupedOptions, and MultiSelect stories are well-implemented.

These stories effectively demonstrate different configurations of the InputCombobox:

  • WithInput shows the component with an input field.
  • GroupedOptions showcases the ability to group options, which is a valuable feature.
  • MultiSelect demonstrates the multi-selection capability.

The use of ...Default.args promotes code reusability and maintains consistency across stories.


90-96: LGTM: Clearable story demonstrates the clearable feature.

The Clearable story effectively shows the component's ability to clear a pre-selected value. It builds upon the Default story, modifying only the necessary props to showcase this feature.


98-103: LGTM: WithoutSearch story demonstrates the component without search.

The WithoutSearch story effectively shows the InputCombobox without search functionality. It appropriately reuses the Default args and modifies only the showSearch prop to showcase this configuration.


1-103: Overall: Excellent implementation of Storybook stories for InputCombobox.

This file provides a comprehensive set of stories that effectively demonstrate the various features and configurations of the InputCombobox component. The code is well-structured, consistent, and promotes reusability. It aligns well with Storybook best practices and will serve as valuable documentation for the component.

Some key strengths:

  1. Clear and informative metadata
  2. Reusable common options
  3. Stories covering different configurations (default, with input, grouped options, multi-select, clearable, without search)
  4. Consistent use of props across stories

Great job on creating these stories!

apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils.tsx (1)

26-26: ⚠️ Potential issue

Correct the regex pattern in formatTextWithSlashes

The regular expression in formatTextWithSlashes may not correctly match text enclosed in slashes due to improper escaping. The ending slash should not be escaped. The corrected regex ensures that text between slashes is properly captured.

Apply this diff to fix the regex pattern:

- const regex = /\/(.*?)\\/g;
+ const regex = /\/(.*?)\//g;

Likely invalid or redundant comment.

Comment on lines +207 to +215
const isSelected = (option: TComboboxOption) => {
if (typeof localValue === "object") {
if (Array.isArray(localValue)) {
return localValue.find((item) => item === option.value) !== undefined;
}

return localValue === option.value;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure isSelected function always returns a boolean

The isSelected function may not return a value in all code paths. If localValue is not an object, the function does not return anything, which results in undefined. To avoid potential bugs, the function should always return a boolean value.

Apply this diff to return false when localValue is not an object:

 const isSelected = (option: TComboboxOption) => {
   if (typeof localValue === "object") {
     if (Array.isArray(localValue)) {
       return localValue.find((item) => item === option.value) !== undefined;
     }
     return localValue === option.value;
   }
+  return false;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isSelected = (option: TComboboxOption) => {
if (typeof localValue === "object") {
if (Array.isArray(localValue)) {
return localValue.find((item) => item === option.value) !== undefined;
}
return localValue === option.value;
}
};
const isSelected = (option: TComboboxOption) => {
if (typeof localValue === "object") {
if (Array.isArray(localValue)) {
return localValue.find((item) => item === option.value) !== undefined;
}
return localValue === option.value;
}
return false;
};

Comment on lines +166 to +184
return localValue.map((item, idx) => {
const option = validOptions?.find((opt) => opt.value === item);

if (!option) {
return null;
}

return (
<>
{idx !== 0 && <span>,</span>}
<div className="flex items-center gap-2">
{option.icon && <option.icon className="h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && <Image src={option.imgSrc} alt={option.label} width={24} height={24} />}
<span>{option.label}</span>
</div>
</>
);
});
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing key prop to elements in list rendering

In the localValue.map function (lines 166-184), the returned elements are wrapped in fragments <>...</>, which are missing a key prop. React requires that elements in a list have a unique key prop to help identify which items have changed.

Apply this diff to fix the missing key prop:

 return localValue.map((item, idx) => {
   const option = validOptions?.find((opt) => opt.value === item);

   if (!option) {
     return null;
   }

-  return (
-    <>
+  return (
+    <React.Fragment key={item}>
       {idx !== 0 && <span>,</span>}
       <div className="flex items-center gap-2">
         {option.icon && <option.icon className="h-5 w-5 shrink-0 text-slate-400" />}
         {option.imgSrc && <Image src={option.imgSrc} alt={option.label} width={24} height={24} />}
         <span>{option.label}</span>
       </div>
-    </>
+    </React.Fragment>
   );
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return localValue.map((item, idx) => {
const option = validOptions?.find((opt) => opt.value === item);
if (!option) {
return null;
}
return (
<>
{idx !== 0 && <span>,</span>}
<div className="flex items-center gap-2">
{option.icon && <option.icon className="h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && <Image src={option.imgSrc} alt={option.label} width={24} height={24} />}
<span>{option.label}</span>
</div>
</>
);
});
} else {
return localValue.map((item, idx) => {
const option = validOptions?.find((opt) => opt.value === item);
if (!option) {
return null;
}
return (
<React.Fragment key={item}>
{idx !== 0 && <span>,</span>}
<div className="flex items-center gap-2">
{option.icon && <option.icon className="h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && <Image src={option.imgSrc} alt={option.label} width={24} height={24} />}
<span>{option.label}</span>
</div>
</React.Fragment>
);
});
} else {
🧰 Tools
🪛 Biome

[error] 176-176: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +295 to +321
<>
{idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
<CommandGroup heading={group.label}>
{group.options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleSelect(option)}
className="cursor-pointer truncate">
{showCheckIcon && isSelected(option) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
)}
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && (
<Image
src={option.imgSrc}
alt={option.label}
width={24}
height={24}
className="mr-2 shrink-0"
/>
)}
<span className="truncate">{option.label}</span>
</CommandItem>
))}
</CommandGroup>
</>
))}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing key prop to elements in list rendering

In the groupedOptions.map function (lines 295-321), the returned elements are wrapped in fragments <>...</>, which are missing a key prop. Each element in a list should have a unique key prop to help React track changes.

Apply this diff to fix the missing key prop:

 {groupedOptions?.map((group, idx) => (
-  <>
+  <React.Fragment key={group.value}>
     {idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
     <CommandGroup heading={group.label}>
       {group.options.map((option) => (
         <CommandItem
           key={option.value}
           onSelect={() => handleSelect(option)}
           className="cursor-pointer truncate">
           {showCheckIcon && isSelected(option) && (
             <CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
           )}
           {option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
           {option.imgSrc && (
             <Image
               src={option.imgSrc}
               alt={option.label}
               width={24}
               height={24}
               className="mr-2 shrink-0"
             />
           )}
           <span className="truncate">{option.label}</span>
         </CommandItem>
       ))}
     </CommandGroup>
-  </>
+  </React.Fragment>
 ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<>
{idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
<CommandGroup heading={group.label}>
{group.options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleSelect(option)}
className="cursor-pointer truncate">
{showCheckIcon && isSelected(option) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
)}
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && (
<Image
src={option.imgSrc}
alt={option.label}
width={24}
height={24}
className="mr-2 shrink-0"
/>
)}
<span className="truncate">{option.label}</span>
</CommandItem>
))}
</CommandGroup>
</>
))}
{groupedOptions?.map((group, idx) => (
<React.Fragment key={group.value}>
{idx !== 0 && <CommandSeparator key={idx} className="bg-slate-300" />}
<CommandGroup heading={group.label}>
{group.options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => handleSelect(option)}
className="cursor-pointer truncate">
{showCheckIcon && isSelected(option) && (
<CheckIcon className="mr-2 h-4 w-4 text-slate-300 hover:text-slate-400" />
)}
{option.icon && <option.icon className="mr-2 h-5 w-5 shrink-0 text-slate-400" />}
{option.imgSrc && (
<Image
src={option.imgSrc}
alt={option.label}
width={24}
height={24}
className="mr-2 shrink-0"
/>
)}
<span className="truncate">{option.label}</span>
</CommandItem>
))}
</CommandGroup>
</React.Fragment>
))}
🧰 Tools
🪛 Biome

[error] 297-297: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

Comment on lines +193 to +757
export const getMatchValueProps = (
condition: TSingleCondition,
localSurvey: TSurvey
): {
show?: boolean;
showInput?: boolean;
inputType?: HTMLInputTypeAttribute;
options: TComboboxGroupedOption[];
} => {
if (
[
"isAccepted",
"isBooked",
"isClicked",
"isCompletelySubmitted",
"isPartiallySubmitted",
"isSkipped",
"isSubmitted",
].includes(condition.operator)
) {
return { show: false, options: [] };
}

let questions = localSurvey.questions ?? [];
let variables = localSurvey.variables ?? [];
let hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];

const selectedQuestion = questions.find((question) => question.id === condition.leftOperand.value);
const selectedVariable = variables.find((variable) => variable.id === condition.leftOperand.value);

if (condition.leftOperand.type === "question") {
questions = questions.filter((question) => question.id !== condition.leftOperand.value);
} else if (condition.leftOperand.type === "variable") {
variables = variables.filter((variable) => variable.id !== condition.leftOperand.value);
} else if (condition.leftOperand.type === "hiddenField") {
hiddenFields = hiddenFields.filter((field) => field !== condition.leftOperand.value);
}

if (condition.leftOperand.type === "question") {
if (selectedQuestion?.type === TSurveyQuestionTypeEnum.OpenText) {
const allowedQuestionTypes = [TSurveyQuestionTypeEnum.OpenText];

if (selectedQuestion.inputType === "number") {
allowedQuestionTypes.push(TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS);
}

if (["equals", "doesNotEqual"].includes(condition.operator)) {
if (selectedQuestion.inputType !== "number") {
allowedQuestionTypes.push(
TSurveyQuestionTypeEnum.Date,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
TSurveyQuestionTypeEnum.MultipleChoiceMulti
);
}
}

const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type));

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const variableOptions = variables
.filter((variable) =>
selectedQuestion.inputType !== "number" ? variable.type === "text" : variable.type === "number"
)
.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
return {
show: true,
showInput: true,
inputType: selectedQuestion.inputType === "number" ? "number" : "text",
options: groupedOptions,
};
} else if (
selectedQuestion?.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
selectedQuestion?.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
) {
const choices = selectedQuestion.choices.map((choice) => {
return {
label: getLocalizedValue(choice.label, "default"),
value: choice.id,
meta: {
type: "static",
},
};
});

return {
show: true,
showInput: false,
options: [{ label: "Choices", value: "choices", options: choices }],
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.PictureSelection) {
const choices = selectedQuestion.choices.map((choice, idx) => {
return {
imgSrc: choice.imageUrl,
label: `Picture ${idx + 1}`,
value: choice.id,
meta: {
type: "static",
},
};
});

return {
show: true,
showInput: false,
options: [{ label: "Choices", value: "choices", options: choices }],
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.Rating) {
const choices = Array.from({ length: selectedQuestion.range }, (_, idx) => {
return {
label: `${idx + 1}`,
value: idx + 1,
meta: {
type: "static",
},
};
});

const numberVariables = variables.filter((variable) => variable.type === "number");

const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (choices.length > 0) {
groupedOptions.push({
label: "Choices",
value: "choices",
options: choices,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

return {
show: true,
showInput: false,
options: groupedOptions,
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.NPS) {
const choices = Array.from({ length: 11 }, (_, idx) => {
return {
label: `${idx}`,
value: idx,
meta: {
type: "static",
},
};
});

const numberVariables = variables.filter((variable) => variable.type === "number");

const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (choices.length > 0) {
groupedOptions.push({
label: "Choices",
value: "choices",
options: choices,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

return {
show: true,
showInput: false,
options: groupedOptions,
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.Date) {
const openTextQuestions = questions.filter((question) =>
[TSurveyQuestionTypeEnum.OpenText, TSurveyQuestionTypeEnum.Date].includes(question.type)
);

const questionOptions = openTextQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const stringVariables = variables.filter((variable) => variable.type === "text");

const variableOptions = stringVariables.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

return {
show: true,
showInput: true,
inputType: "date",
options: groupedOptions,
};
}
} else if (condition.leftOperand.type === "variable") {
if (selectedVariable?.type === "text") {
const allowedQuestionTypes = [
TSurveyQuestionTypeEnum.OpenText,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
];

if (["equals", "doesNotEqual"].includes(condition.operator)) {
allowedQuestionTypes.push(TSurveyQuestionTypeEnum.MultipleChoiceMulti, TSurveyQuestionTypeEnum.Date);
}

const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type));

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const stringVariables = variables.filter((variable) => variable.type === "text");

const variableOptions = stringVariables.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

return {
show: true,
showInput: true,
inputType: "text",
options: groupedOptions,
};
} else if (selectedVariable?.type === "number") {
const allowedQuestions = questions.filter((question) =>
[TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS].includes(question.type)
);

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const numberVariables = variables.filter((variable) => variable.type === "number");

const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

return {
show: true,
showInput: true,
inputType: "number",
options: groupedOptions,
};
}
} else if (condition.leftOperand.type === "hiddenField") {
const allowedQuestionTypes = [
TSurveyQuestionTypeEnum.OpenText,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
];

if (["equals", "doesNotEqual"].includes(condition.operator)) {
allowedQuestionTypes.push(TSurveyQuestionTypeEnum.MultipleChoiceMulti, TSurveyQuestionTypeEnum.Date);
}

const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type));

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const variableOptions = variables
.filter((variable) => variable.type === "text")
.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

return {
show: true,
showInput: true,
inputType: "text",
options: groupedOptions,
};
}

return { show: false, options: [] };
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor the getMatchValueProps function to improve maintainability

The getMatchValueProps function spans over 560 lines and contains multiple nested conditional statements with repeated code blocks for handling different operand types and question types. This complexity makes the code difficult to read, maintain, and test.

Consider refactoring the function by extracting helper functions for repetitive code segments, such as generating grouped options or handling specific question types. Breaking down the function into smaller, focused functions will enhance readability and make future modifications easier.

For example, you could create a helper function to generate grouped options:

const generateGroupedOptions = (
  questions: TSurveyQuestion[],
  variables: TSurveyVariable[],
  hiddenFields: string[]
): TComboboxGroupedOption[] => {
  // Implementation to generate grouped options
};

Then, use this helper function in getMatchValueProps wherever grouped options are constructed.

Comment on lines +165 to +169
export const getDefaultOperatorForQuestion = (question: TSurveyQuestion): TSurveyLogicConditionsOperator => {
const options = getQuestionOperatorOptions(question);

return options[0].value.toString() as TSurveyLogicConditionsOperator;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add a safety check for empty options array in getDefaultOperatorForQuestion

The function getDefaultOperatorForQuestion assumes that getQuestionOperatorOptions will always return a non-empty array. If options is empty, accessing options[0] will result in an undefined value, potentially causing runtime errors.

Consider adding a check to ensure that the options array is not empty before accessing options[0]. For example:

export const getDefaultOperatorForQuestion = (question: TSurveyQuestion): TSurveyLogicConditionsOperator => {
  const options = getQuestionOperatorOptions(question);
+ if (options.length === 0) {
+   throw new Error(`No operator options available for question type ${question.type}`);
+ }
  return options[0].value.toString() as TSurveyLogicConditionsOperator;
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getDefaultOperatorForQuestion = (question: TSurveyQuestion): TSurveyLogicConditionsOperator => {
const options = getQuestionOperatorOptions(question);
return options[0].value.toString() as TSurveyLogicConditionsOperator;
};
export const getDefaultOperatorForQuestion = (question: TSurveyQuestion): TSurveyLogicConditionsOperator => {
const options = getQuestionOperatorOptions(question);
if (options.length === 0) {
throw new Error(`No operator options available for question type ${question.type}`);
}
return options[0].value.toString() as TSurveyLogicConditionsOperator;
};

Comment on lines +847 to +985
export const getActionValueOptions = (variableId: string, localSurvey: TSurvey): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
let variables = localSurvey.variables ?? [];
const questions = localSurvey.questions;

const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});

const selectedVariable = variables.find((variable) => variable.id === variableId);

variables = variables.filter((variable) => variable.id !== variableId);

if (!selectedVariable) return [];

if (selectedVariable.type === "text") {
const allowedQuestions = questions.filter((question) =>
[
TSurveyQuestionTypeEnum.OpenText,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
TSurveyQuestionTypeEnum.Rating,
TSurveyQuestionTypeEnum.NPS,
TSurveyQuestionTypeEnum.Date,
].includes(question.type)
);

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const stringVariables = variables.filter((variable) => variable.type === "text");

const variableOptions = stringVariables.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

return groupedOptions;
} else if (selectedVariable.type === "number") {
const allowedQuestions = questions.filter((question) =>
[TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS].includes(question.type)
);

const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});

const numberVariables = variables.filter((variable) => variable.type === "number");

const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});

const groupedOptions: TComboboxGroupedOption[] = [];

if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}

if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}

if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure selectedVariable is defined before proceeding

In the getActionValueOptions function, if selectedVariable is not found (i.e., it is undefined), the function returns an empty array. However, the code continues to process selectedVariable afterward, which could lead to runtime errors.

Consider adding an early return after checking if selectedVariable is undefined to prevent further execution:

const selectedVariable = variables.find((variable) => variable.id === variableId);

variables = variables.filter((variable) => variable.id !== variableId);

if (!selectedVariable) return [];

+ // Ensure selectedVariable is defined before proceeding

Committable suggestion was skipped due to low confidence.

@mattinannt mattinannt dismissed Dhruwang’s stale review September 30, 2024 10:16

requested changes have been addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants