Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Recall functionality #1789

Merged
merged 50 commits into from
Jan 17, 2024
Merged

feat: Recall functionality #1789

merged 50 commits into from
Jan 17, 2024

Conversation

Dhruwang
Copy link
Contributor

What does this PR do?

Fixes #1673

recall.mp4

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change adds a new database migration
  • This change requires a documentation update

How should this be tested?

Type @ into question headline box and select the recall question

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read How we Code at Formbricks
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand bits
  • Ran pnpm build
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues
  • First PR at Formbricks? Please sign the CLA! Without it we wont be able to merge it 🙏

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Formbricks Docs if changes were necessary

Copy link

vercel bot commented Dec 18, 2023

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

2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
formbricks-cloud ⬜️ Ignored (Inspect) Visit Preview Jan 17, 2024 1:25pm
formbricks-com ⬜️ Ignored (Inspect) Visit Preview Jan 17, 2024 1:25pm

Copy link
Contributor

github-actions bot commented Dec 18, 2023

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

Copy link
Contributor

apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm.tsx

Consider using the useCallback hook for the event handlers to avoid unnecessary re-rendering of the components. This will improve the performance of your application.
Create Issue
See the diff
Checkout the fix

    const handleInputChange = useCallback((inputType: TSurveyOpenTextQuestionInputType) => {
      const updatedAttributes = {
        inputType: inputType,
        placeholder: getPlaceholderByInputType(inputType),
        longAnswer: inputType === "text" ? question.longAnswer : false,
      };
      updateQuestion(questionIdx, updatedAttributes);
    }, [question.longAnswer, questionIdx, updateQuestion]);
git fetch origin && git checkout -b ReviewBot/Impro-59912zj origin/ReviewBot/Impro-59912zj

apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/RecallQuestionSelect.tsx

Consider destructuring the properties of the objects to improve the readability of your code.
Create Issue
See the diff
Checkout the fix

    const { environmentId, questions } = localSurvey;
git fetch origin && git checkout -b ReviewBot/Impro-4awb89n origin/ReviewBot/Impro-4awb89n

packages/surveys/src/components/questions/CTAQuestion.tsx

Consider validating the URL before opening it in a new tab to prevent any potential security risks.
Create Issue
See the diff
Checkout the fix

    if (question.buttonExternal && question.buttonUrl && isValidURL(question.buttonUrl)) {
      window?.open(question.buttonUrl, "_blank")?.focus();
    }
git fetch origin && git checkout -b ReviewBot/Impro-xr97qem origin/ReviewBot/Impro-xr97qem

packages/surveys/src/components/general/Survey.tsx

Consider breaking down the getNextQuestionId function into smaller, more manageable functions. This will make the code easier to read and maintain.
Create Issue
See the diff
Checkout the fix

    const getNextQuestionId = (data: TResponseData, isFromPrefilling: Boolean = false): string => {
      const questions = survey.questions;
      const responseValue = data[questionId];

      if (questionId === "start") {
        if (!isFromPrefilling) {
          return questions[0]?.id || "end";
        } else {
          currIdx = 0;
          currQues = questions[0];
        }
      }
      if (currIdx === -1) throw new Error("Question not found");

      if (currQues?.logic && currQues?.logic.length > 0) {
        for (let logic of currQues.logic) {
          if (!logic.destination) continue;

          if (evaluateCondition(logic, responseValue)) {
            return logic.destination;
          }
        }
      }
      return questions[currIdx + 1]?.id || "end";
    }
git fetch origin && git checkout -b ReviewBot/Impro-flx62rf origin/ReviewBot/Impro-flx62rf

Consider using the useCallback hook to memoize the onSubmit function. This will prevent unnecessary re-rendering of the component when the function is passed as a prop to child components.
Create Issue
See the diff
Checkout the fix

    const onSubmit = useCallback((responseData: TResponseData, ttc: TResponseTtc, isFromPrefilling: Boolean = false) => {
      const questionId = Object.keys(responseData)[0];
      setLoadingElement(true);
      const nextQuestionId = getNextQuestionId(responseData, isFromPrefilling);
      const finished = nextQuestionId === "end";
      onResponse({ data: responseData, ttc, finished });
      if (finished) {
        onFinished();
      }
      setQuestionId(nextQuestionId);
      // add to history
      setHistory([...history, questionId]);
      setLoadingElement(false);
      onActiveQuestionChange(nextQuestionId);
    }, [getNextQuestionId, onResponse, onFinished, setQuestionId, setHistory, setLoadingElement, onActiveQuestionChange]);
git fetch origin && git checkout -b ReviewBot/Impro-rmqdas3 origin/ReviewBot/Impro-rmqdas3

Consider renaming the variable 'ttc' to a more descriptive name. This will make the code easier to understand for other developers.
Create Issue
See the diff
Checkout the fix

    const [responseTime, setResponseTime] = useState<TResponseTtc>({});
git fetch origin && git checkout -b ReviewBot/Impro-np1jx2z origin/ReviewBot/Impro-np1jx2z

packages/lib/utils/recall.ts

The same regular expression pattern is used in multiple places. It would be more readable and maintainable to define it once and reuse it.
Create Issue
See the diff
Checkout the fix

    const RECALL_PATTERN = /recall:([A-Za-z0-9]+)/;
    const FALLBACK_PATTERN = /fallback:(\S*)/;
    const RECALL_INFO_PATTERN = /recall:([A-Za-z0-9]+)\/fallback:(\S*)/;
    ...
git fetch origin && git checkout -b ReviewBot/Impro-snj1cfx origin/ReviewBot/Impro-snj1cfx

};

export default function RecallQuestionSelect({
currentQuestionIdx,
Copy link
Contributor

Choose a reason for hiding this comment

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

Destructuring the properties of the 'localSurvey' object to improve the readability of the code.

Suggested change
currentQuestionIdx,
const { environmentId, questions } = localSurvey;

Comment on lines 1 to 64
import { TSurvey } from "@formbricks/types/surveys";

export function extractId(text: string) {
const pattern = /recall:([A-Za-z0-9]+)/;
const match = text.match(pattern);
if (match && match[1]) {
return match[1];
} else {
return null;
}
}

export function extractIds(text: string) {
const pattern = /recall:([A-Za-z0-9]+)/g;
const matches = text.match(pattern);
if (!matches) {
return [];
}
return matches
.map((match) => {
const matchPattern = /recall:([A-Za-z0-9]+)/;
const idMatch = match.match(matchPattern);
return idMatch ? idMatch[1] : null;
})
.filter((id) => id !== null);
}

export function extractFallbackValue(text: string): string {
const pattern = /fallback:(\S*)/;
const match = text.match(pattern);
if (match && match[1]) {
return match[1];
} else {
return "";
}
}

export function extractRecallInfo(headline: string): string | null {
const pattern = /recall:([A-Za-z0-9]+)\/fallback:(\S*)/;
const match = headline.match(pattern);
return match ? match[0] : null;
}

export const checkForRecall = (headline: string, survey: TSurvey) => {
let newHeadline = headline;
if (!headline.includes("recall:")) return headline;
while (newHeadline.includes("recall:")) {
const recallInfo = extractRecallInfo(newHeadline);
if (recallInfo) {
const questionId = extractId(recallInfo);
newHeadline = newHeadline.replace(
recallInfo,
`@${survey.questions.find((question) => question.id === questionId)?.headline}`
);
}
}
return newHeadline;
};

export function findRecallInfoById(text: string, id: string): string | null {
const pattern = new RegExp(`recall:${id}\\/fallback:(\\S*)`, "g");
const match = text.match(pattern);
return match ? match[0] : null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Defining the regular expression patterns once and reusing them in the code to improve readability and maintainability.

Suggested change
import { TSurvey } from "@formbricks/types/surveys";
export function extractId(text: string) {
const pattern = /recall:([A-Za-z0-9]+)/;
const match = text.match(pattern);
if (match && match[1]) {
return match[1];
} else {
return null;
}
}
export function extractIds(text: string) {
const pattern = /recall:([A-Za-z0-9]+)/g;
const matches = text.match(pattern);
if (!matches) {
return [];
}
return matches
.map((match) => {
const matchPattern = /recall:([A-Za-z0-9]+)/;
const idMatch = match.match(matchPattern);
return idMatch ? idMatch[1] : null;
})
.filter((id) => id !== null);
}
export function extractFallbackValue(text: string): string {
const pattern = /fallback:(\S*)/;
const match = text.match(pattern);
if (match && match[1]) {
return match[1];
} else {
return "";
}
}
export function extractRecallInfo(headline: string): string | null {
const pattern = /recall:([A-Za-z0-9]+)\/fallback:(\S*)/;
const match = headline.match(pattern);
return match ? match[0] : null;
}
export const checkForRecall = (headline: string, survey: TSurvey) => {
let newHeadline = headline;
if (!headline.includes("recall:")) return headline;
while (newHeadline.includes("recall:")) {
const recallInfo = extractRecallInfo(newHeadline);
if (recallInfo) {
const questionId = extractId(recallInfo);
newHeadline = newHeadline.replace(
recallInfo,
`@${survey.questions.find((question) => question.id === questionId)?.headline}`
);
}
}
return newHeadline;
};
export function findRecallInfoById(text: string, id: string): string | null {
const pattern = new RegExp(`recall:${id}\\/fallback:(\\S*)`, "g");
const match = text.match(pattern);
return match ? match[0] : null;
}
const RECALL_PATTERN = /recall:([A-Za-z0-9]+)/;
const RECALL_PATTERN_GLOBAL = /recall:([A-Za-z0-9]+)/g;
const FALLBACK_PATTERN = /fallback:(\S*)/;
const RECALL_INFO_PATTERN = /recall:([A-Za-z0-9]+)\/fallback:(\S*)/;
export function extractId(text: string) {
const match = text.match(RECALL_PATTERN);
...
}
export function extractIds(text: string) {
const matches = text.match(RECALL_PATTERN_GLOBAL);
...
const idMatch = match.match(RECALL_PATTERN);
...
}
export function extractFallbackValue(text: string): string {
const match = text.match(FALLBACK_PATTERN);
...
}
export function extractRecallInfo(headline: string): string | null {
const match = headline.match(RECALL_INFO_PATTERN);
...
}
export function findRecallInfoById(text: string, id: string): string | null {
const pattern = new RegExp(`recall:${id}\/fallback:(\S*)`, "g");
...
}

@jobenjada
Copy link
Member

jobenjada commented Dec 19, 2023

Hey Dru!

Super cool what you've built with so few lines of code 😍 💪 Here is my feedback:

  1. On click on "Edit fallback" I got this error:
image
  1. Pls add a bit of padding-x and a space between the @ and the question text:
image

Also, add a bit of margin-x here:

image
  1. Edit fallback button behavior
image
  • This button behaves a bit odd. It appears when it should, like here when you hover with the mouse over the question field or the pop out:
image
  • Its hard to reach because it disappears when you leave the text area with the cursor. So you have to move along the text area to the far right to then move down to click it.

Can we move it into this bubble and display it on bubble hover? And then open the fallback editor on bubble click?
image

That would be much more intuitive and solve the hover issues :)

When there are two recalled questions, only display the fallback settings for the one where the user clicked on the bubble:
image


  1. Filter list if question was already added: I can add the same info twice
image
  1. Enable for descriptions and question type File Upload as well :)
image image
  1. Deleting behaviour: Remove the whole bubble instead of unlinking it and displaying the question text
image

So if you hit backspace, just remove the bubble.


  1. Resolve bubbleception

Recalling questions which are recalling questions leads to tiny UI mess:

image image image image

Any ideas on how to solve that? 🤔 I think it doesnt make sense to have it in the bubble itself


  1. With a lot of questions, the UI breaks:
image

and the icons are getting smaller and smaller:

image
  1. Add space between two values when two values are chosen in multi select:
image
  1. Filter out questions of the following types from the list of questions to recall from: Picture Choice, Call to Action, Consent, File Upload.
image
  1. Question Type Date: Formatting

This is a recalled date:

image

Can we reformat that to "12th of December, 2023" (or similar) ? Just to increase readability.


Seems like a lot but mostly tweaking! Great work Dru, really impressed with this 😍 💪

@jobenjada jobenjada self-requested a review December 19, 2023 00:26
@mattinannt
Copy link
Member

@Dhruwang I'm currently making the final testing. The whole feature looks and works pretty great! 😊💪
There was only one issue I identified:

The recall information in the thank you card isn't working:
https://github.com/formbricks/formbricks/assets/675065/41726b33-317c-47a8-b527-9453c1c4388c

Can you please check and fix that? If this is fixed we are ready to merge this 😊💪

@jobenjada
Copy link
Member

Added little hint as placeholder:

image

@Dhruwang
Copy link
Contributor Author

@mattinannt , have fixed the issue with thank you card 😊

Copy link
Member

@mattinannt mattinannt left a comment

Choose a reason for hiding this comment

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

@Dhruwang thanks for the changes; works great! 😊
So cool that we finally finished this feature after so many months and different attempts. great job! 👏👏

@mattinannt mattinannt added this pull request to the merge queue Jan 17, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 17, 2024
@mattinannt
Copy link
Member

@Dhruwang there seems to be one issue left with the e2e tests. Can you please check what's going on there? 😊

@mattinannt mattinannt added this pull request to the merge queue Jan 17, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 17, 2024
@mattinannt mattinannt added this pull request to the merge queue Jan 17, 2024
Merged via the queue into main with commit f280063 Jan 17, 2024
12 checks passed
@mattinannt mattinannt deleted the recall branch January 17, 2024 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants