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

Fix/con/898 ensure exclusive acceptance of mentors #914

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

ericbolikowski
Copy link
Contributor

@ericbolikowski ericbolikowski commented May 19, 2024

What Github issue does this PR relate to? Insert link.

#898

This PR fixes bug #898 which allowed mentees in special cases to have two simultaneously active/accepted Mentorship Matches:
CleanShot 2024-05-19 at 11 28 22@2x|200

The case could happen when for example:

  1. Eric sends mentorship requests to two mentors, Anil and Kate
  2. Two Mentorship Matches are created, both with state Applied
  3. Anil and Kate access the CON Applications page at the same time
  4. Anil accepts the mentorship
  5. Nestjs updates the two Mentorship Mathches:
  • The Eric+Anil Mentorship Match becomes Accepted
  • The Eric+Kate Mentorship Match becomes Invalidated/Cancelled
  1. Kate accepts the mentorship
  2. The Eric+Kate Mentorship Match becomes Accepted

The fix was obvious: in the code that handles the "accept mentorship" mutation, only change the Mentorship Match state into Accepted if the current state is Applied.

What should the reviewer know?

The backend issue is solved, and we likely won't get into a state of one mentee + two active mentorships. But an unpleasant UX issue came up:

If the above steps happened again, but with the fix in this PR, Kate would in step 6 be prevented from accepting the mentorship request. Nestjs would return an error handled by the CON frontend in the catch (error) { clause you'll see. I've implemented a crude alert() shown to the user and then the page refreshes.

That scenario is illustrated in this Loom:
https://www.loom.com/share/47fb8764f8d14c619092a3c8a6775b6d?sid=1dd21bcc-d13f-4020-a133-7a91d5bbb265

This leads to the loss of the acceptance message that Kate wrote - which is annoying, but the message isn't relevant any longer since Eric has a mentorship with Anil and isn't supposed to have one with Kate.

The question is: are there any easy ways to improve this UX?

Summary by CodeRabbit

  • New Features

    • Added a check to ensure mentorship matches are in the "APPLIED" state before accepting.
    • Introduced periodic data refetching every 60 seconds for mentorship matches.
  • Enhancements

    • Added an alert message and page reload in the error handling of mentorship confirmation.
  • Chores

    • Removed a console.log statement from the company profile editables component.

…etails)

this prevents a scenario where both Mentor A and B, both of them having received a mentorship application from Mentee C, go to the Applications page, and Mentor A his application first, and thereafter Mentor B then accepts his application. The acceptance by Mentor A of the first mentorship match will invalidate the second mentorship match sent to Mentor B
Copy link

coderabbitai bot commented May 19, 2024

Walkthrough

The recent changes primarily focus on enhancing the mentorship matching workflow. A critical check was added to ensure mentorships are only accepted when in the "APPLIED" state. Error handling improvements and periodic data refetch mechanisms were also introduced across various components. Additionally, unnecessary logging was removed from an unrelated file.

Changes

Files Change Summary
apps/nestjs-api/src/con-mentorship-matches/con-mentorship-matches.service.ts Added a state check to ensure mentorship matches are in the "APPLIED" state before accepting them.
apps/redi-connect/src/components/organisms/ConfirmMentorship.tsx Added a new alert message and a page reload in error handling.
apps/redi-connect/src/pages/app/applications/Applications.tsx Modified useGetMentorshipMatchesQuery call to include a {} and { refetchInterval: 60 * 1000 } for periodic refetching.
apps/redi-connect/src/pages/app/applications/ApplicationsFilterContext.tsx Added a configuration object with refetchInterval to useGetMentorshipMatchesQuery for setting a 60 seconds refetch interval.
apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableNamePhotoLocation.tsx Removed a console.log statement related to myData.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as ConfirmMentorship UI
    participant APICall as acceptMentorship API Call
    participant Service as ConMentorshipMatchesService
    
    User->>UI: Click Accept Mentorship
    UI->>APICall: Send Accept Request
    APICall->>Service: Handle Request
    Service->>Service: Check if match is in "APPLIED" state
    alt State is "APPLIED"
        Service->>APICall: Confirm Success
        APICall->>UI: Success Response
        UI->>User: Show Success Message and Reload Page
    else State is not "APPLIED"
        Service->>APICall: Reject with Error
        APICall->>UI: Error Response
        UI->>User: Show Error Message and Reload Page
    end

Poem

In the lands where mentors guide,
Checks and matches must abide.
Errors caught, with alerts to say,
Keeping troubles well at bay.
Data fetched as seconds tick,
And logs removed, oh so slick.
Every change, a better way,
To mentor dreams, and light the way. 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@katamatata
Copy link
Contributor

katamatata commented May 22, 2024

@ericbolikowski, I think the better way could be to inform them about application invalidation on the platform as well and not let them accept it. But I don't understand why the mentor sees the Accept & Decline buttons on the Invalidated application. They shouldn't:
image

@astkhikatredi
Copy link
Contributor

Hey @ericbolikowski, thanks for the PR and questions. Dear @katamatata, thanks for the comment.

Here are my thoughts, what do you think?

1. Move Application to Cancelled:
Automatically move Kate's application to the Cancelled state once Anil accepts the mentorship.
2. Hide Accept and Decline Buttons:
Ensure that Kate no longer sees the Accept andDeclinebuttons, preventing her from accepting the application.
3. Insert Notification:
Provide a clear and concise notification to inform Kate about the status change and the reason behind it, for example: "Sorry, this mentorship request cannot be accepted anymore because the mentee has already been accepted by another mentor. As a result, we have moved this application to "Cancelled". We hope you will receive another application very soon."

Thanks!

@ericbolikowski
Copy link
Contributor Author

Ah, we're getting into the nasty territory of concurrency and simultaneity here.

First, a brainfuck: we're already doing what @astkhikatredi suggests in her points (1) and (2). And @katamatata, yes, we don't show Accept/Decline buttons when application is Invalidated.

Here's why - let me illustrate with the example again:

  • Eric sends applications to Anil and Kate. Both applications are in state Applied
  • Within seconds of one another (this is important), both Anil and Kate visit the Applications page
  • Anil now sees the application he's received. Kate sees the application she's received
  • Anil accepts the application. That application becomes Accepted. The application to Kate becomes Invalidated
  • ... buuuuuut: Kate's browser still thinks the application is Applied. That's why it still shows Kate the Accept / Decline buttons. If Kate had refreshed the browser, then they would have disappeared

@ericbolikowski
Copy link
Contributor Author

Writing this made me think about a simple fix: the refresh. We can very easily update our code so that Kate's browser asks the server every five or ten seconds to give it an up-to-date application, so that it can become aware of a change from Applied to Invalidated. Then we will show what we already show to hundreds of mentors that have invalidated applications: the status message "Cancelled".

We don't give a lot of context to tell the mentor in the browser what "Cancelled" means (that's done in a lot of detail in the email they receive), but we can quite easily also add some info bubble/card that says something like "This application was cancelled since another mentor accepted an application from this mentee".

@katamatata
Copy link
Contributor

Writing this made me think about a simple fix: the refresh. We can very easily update our code so that Kate's browser asks the server every five or ten seconds to give it an up-to-date application, so that it can become aware of a change from Applied to Invalidated.

I thought about the refresh, too, and thought we were already doing it. Probably not that often.

@ericbolikowski
Copy link
Contributor Author

We do all our data loading using react-query. React-query does its queries using a query client what we configure and give it. Here's how we do it in both CON and TP:

code

Note cacheTime here. The difference between cacheTime and its cousins staleTime and refetchInterval are subtle, but the gist of it here is that the way we've set up the query client, react-query will caches data for up to 5 minutes before it's marked "out of date", which, in combination with some other parameters leads to a refetch under certain conditions.

We can set these options on a per-query level as well. At the moment we've got apps/redi-connect/src/pages/app/applications/Applications.tsx with this:

function Applications() {
  const mentorshipMatchesQuery = useGetMentorshipMatchesQuery()

that could be changed to:

function Applications() {
  const mentorshipMatchesQuery = useGetMentorshipMatchesQuery(
    {},
    { refetchInterval: 5 * 1000 }
  )

... which will probably have this effect: while user is on the applications page, aggressively refetch the data every five seconds. We'd need to recheck the react-query docs (I just did so very briefly right now), but this is easy to put in place.

@katamatata, maybe a nice learning opportunity? Wanna dig in and then we talk more in next mentoring session?

@astkhikatredi
Copy link
Contributor

Dear @ericbolikowski, any timeline for the implementation?
THANKS!

@ericbolikowski
Copy link
Contributor Author

Hi @astkhikatredi, @katamatata and I aligned this morning:

  • we're likely going for the approach I outlined in a comment here
  • @katamatata is checking the validity of the approach (by looking at react-query docs), or any final tweaks we need to do before implementing it

Most work is already done, so I don't think we're far from completing this ticket.

@katamatata
Copy link
Contributor

@katamatata, maybe a nice learning opportunity? Wanna dig in and then we talk more in next mentoring session?

@ericbolikowski, I apologize for the delayed response and thank you for providing more context on the query implementation!

Upon reviewing the react-query docs, I found that since useQuery considers cached data as stale immediately after fetching, we don't need to use the staleTime: 0 option as the default staleTime is already 0. If we were to pass values greater than 0 to staleTime, it would allow the data to be slightly outdated before triggering a re-fetch, but it's unnecessary in this case.

We indeed can use only the refetchInterval option to customize the mentorshipMatchesQuery in order to automatically re-fetch data in the background and keep it updated at regular intervals without user interaction. Though, I suggest setting refetchInterval to a more reasonable interval - 1 minute:

function Applications() {
  const mentorshipMatchesQuery = useGetMentorshipMatchesQuery(
    {},
    { refetchInterval: 60 * 1000 }
  )

Do you agree?

@ericbolikowski
Copy link
Contributor Author

@katamatata sounds good - thanks for digging into it. I agree with the higher retfetchInterval of a minute.

Can you test this locally? If it works as intended, you should see a new request every minute (or whatever you set, for testing) in the Network tab in DevTools.

@katamatata
Copy link
Contributor

@ericbolikowski, this solution works fine 👍 I tested it & pushed the commit.
As a mentor who started writing an acceptance message in a modal and wasn't fast enough, I see the page reloading and the application with invalidated status (without Accept/Decline buttons). I think besides the email, we should also display a message on the platform to this mentor about the application being invalidated. What do you think?

@ericbolikowski
Copy link
Contributor Author

ericbolikowski commented Jun 14, 2024

@katamatata thank you! And - agreed. Let's talk more about this on Monday morning.

Copy link

@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: 0

Outside diff range and nitpick comments (4)
apps/redi-connect/src/pages/app/applications/Applications.tsx (2)

Line range hint 45-45: Replace the anchor tag with a button for better accessibility.

Using an anchor tag for actions that perform operations other than navigation is not recommended as it can lead to poor accessibility. Consider changing this to a button element to improve accessibility and semantic correctness.

- <a onClick={() => history.push('/app/find-a-mentor')}>
+ <button onClick={() => history.push('/app/find-a-mentor')}>

Line range hint 51-55: Remove unnecessary React fragment.

The React fragment here is redundant since it wraps a single child. Removing it can clean up the code and reduce unnecessary nesting.

- <>
  {profile.userType === 'MENTOR' && (
    <>
      You currently have no mentee applications. To increase your
      chances of receiving an application, make sure that your
      profile is up-to-date, informative and friendly.
    </>
  )}
- </>
+ {profile.userType === 'MENTOR' && (
    You currently have no mentee applications. To increase your
    chances of receiving an application, make sure that your
    profile is up-to-date, informative and friendly.
  )}
apps/redi-connect/src/pages/app/applications/ApplicationsFilterContext.tsx (1)

Line range hint 64-76: Remove unnecessary else clauses to simplify logic.

The else clauses are unnecessary as all previous branches in the filter function return early. Removing them can simplify the code and increase readability.

- else if (activeFilter === 'accepted') {
+ if (activeFilter === 'accepted') {
- else if (activeFilter === 'declined') {
+ if (activeFilter === 'declined') {
- else if (activeFilter === 'cancelled') {
+ if (activeFilter === 'cancelled') {
apps/nestjs-api/src/con-mentorship-matches/con-mentorship-matches.service.ts (1)

Line range hint 45-47: Remove unnecessary else clause to simplify logic.

The else clause after the if condition can be omitted as it is unnecessary and can simplify the control flow of the code.

- else {
+ {
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between b8ba7ae and dd5c448.

Files selected for processing (5)
  • apps/nestjs-api/src/con-mentorship-matches/con-mentorship-matches.service.ts (1 hunks)
  • apps/redi-connect/src/components/organisms/ConfirmMentorship.tsx (1 hunks)
  • apps/redi-connect/src/pages/app/applications/Applications.tsx (1 hunks)
  • apps/redi-connect/src/pages/app/applications/ApplicationsFilterContext.tsx (1 hunks)
  • apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableNamePhotoLocation.tsx (1 hunks)
Files skipped from review due to trivial changes (2)
  • apps/redi-connect/src/components/organisms/ConfirmMentorship.tsx
  • apps/redi-talent-pool/src/components/organisms/company-profile-editables/EditableNamePhotoLocation.tsx
Additional context used
Biome
apps/redi-connect/src/pages/app/applications/Applications.tsx

[error] 45-45: Use a button element instead of an a element. (lint/a11y/useValidAnchor)

Anchor elements should only be used for default sections or page navigation
Check this thorough explanation to better understand the context.


[error] 51-55: Avoid using unnecessary Fragment. (lint/complexity/noUselessFragments)

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.
Unsafe fix: Remove the Fragment

apps/redi-connect/src/pages/app/applications/ApplicationsFilterContext.tsx

[error] 64-76: This else clause can be omitted because previous branches break early. (lint/style/noUselessElse)

Unsafe fix: Omit the else clause.


[error] 69-76: This else clause can be omitted because previous branches break early. (lint/style/noUselessElse)


[error] 71-76: This else clause can be omitted because previous branches break early. (lint/style/noUselessElse)

apps/nestjs-api/src/con-mentorship-matches/con-mentorship-matches.service.ts

[error] 45-47: This else clause can be omitted because previous branches break early. (lint/style/noUselessElse)

Unsafe fix: Omit the else clause.

Additional comments not posted (3)
apps/redi-connect/src/pages/app/applications/Applications.tsx (1)

16-19: Ensure periodic data refresh with a refetch interval of one minute.

The refetchInterval is set to 60 seconds, which should help keep the mentorship match data up-to-date and address the concurrency issue described in the PR.

apps/redi-connect/src/pages/app/applications/ApplicationsFilterContext.tsx (1)

47-50: Ensure periodic data refresh with a refetch interval of one minute.

The refetchInterval is consistently set to 60 seconds across different components, which aligns with the PR objectives to keep data fresh and mitigate race conditions.

apps/nestjs-api/src/con-mentorship-matches/con-mentorship-matches.service.ts (1)

76-81: Ensure mentorship acceptance is contingent on the correct status.

The added condition ensures that a mentorship can only be accepted if it is in the 'APPLIED' state, addressing the concurrency issue highlighted in the PR.

@astkhikatredi
Copy link
Contributor

Hey @katamatata and @ericbolikowski , could you maybe review point 3 in my message above? What do you think about showing it to a mentor with an invalid mentorship application?

3. Insert Notification:
"Sorry, this mentorship request cannot be accepted anymore because the mentee has already been accepted by another mentor. As a result, we have moved this application to "Cancelled". We hope you will receive another application very soon."

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

3 participants