Skip to content

[fix] Facilitators can only withdraw an application before it is accepted#2566

Merged
Will-Howard merged 2 commits into
masterfrom
wh-2551-facilitator-withdraw-application-2026-05
May 31, 2026
Merged

[fix] Facilitators can only withdraw an application before it is accepted#2566
Will-Howard merged 2 commits into
masterfrom
wh-2551-facilitator-withdraw-application-2026-05

Conversation

@Will-Howard

@Will-Howard Will-Howard commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Description

  • Previously: Facilitators could withdraw before they were assigned to a group, both facilitators and participants were shown the full "Drop out or defer" modal
  • Now:
    • Facilitators can only withdraw before they are accepted
    • For the case before someone is accepted, a simpler "Withdraw application" modal is shown (to both facilitators and participants). Behind the scenes this does the same thing as "Drop out"

Issue

Fixes #2551

Developer checklist

Screenshot

Focusing on the withdraw-before-start case

Note: I am aware the padding is a little ugly on the modal. This is coming from the outer Modal component so is fiddly to change.

📸 Before After
📱 Screenshot 2026-05-31 at 11 47 49 Screenshot 2026-05-31 at 11 46 30
Screenshot 2026-05-31 at 11 47 53 Screenshot 2026-05-31 at 11 46 42
Screenshot 2026-05-31 at 11 50 57
🖥️ Screenshot 2026-05-31 at 11 47 22 Screenshot 2026-05-31 at 11 46 11
Screenshot 2026-05-31 at 11 47 31 Screenshot 2026-05-31 at 11 46 21
Screenshot 2026-05-31 at 11 50 47

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Server-side router rejects deferral requests when a registration decision is null, forbids facilitator deferrals, and forbids facilitator withdrawals once a decision exists. Client-side introduces getApplicationActionLabel and shows a "Withdraw application" overflow item only for upcoming rows with null decision. DropoutModal now renders a WithdrawConfirm flow for pre-decision registrations and corresponding Storybook story and tests validate the confirm/response/mutation payload.

Possibly related PRs

  • bluedotimpact/bluedot#1959: The main PR’s participant “drop/deferral” flow changes (new pre-decision “Withdraw application”/WithdrawConfirm in DropoutModal.tsx plus updated dropoutOrDeferral expectations and labels) are directly tied to the retrieved PR’s introduction of the dropout/defer modal and dropoutOrDeferral backend route.
  • bluedotimpact/bluedot#2564: Both PRs modify the same facilitator drop/defer flow—UI labels/options for “Drop out” vs “Defer”, and backend dropoutOrDeferral authorization logic preventing facilitator deferrals (with the main PR extending it to pending decision/withdraw cases).
  • bluedotimpact/bluedot#2502: Both PRs are related because they both wire course-row “drop out / defer” actions into the shared DropoutModal flow, with the main PR adding the pre-decision “Withdraw application” behavior that the settings CTA (retrieved PR) would open.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: restricting facilitator withdrawals to pre-acceptance applications.
Linked Issues check ✅ Passed All coding requirements from issue #2551 are met: facilitators can only withdraw before acceptance (not after), deferral is blocked server-side for facilitators, UI labels are updated, and participants remain unaffected.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue #2551 requirements. Updates to UI labels, server-side validation, tests, and stories are all scoped to the facilitator withdrawal functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description includes all required template sections: explanation of changes, linked issue (#2551), completed developer checklist items, and detailed before/after screenshots.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wh-2551-facilitator-withdraw-application-2026-05

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tightens facilitator withdrawal permissions so that a facilitator can only withdraw their application while it is still under review (decision === null), rather than up until they are assigned to a group.

  • Backend (dropout.ts): The server-side guard now checks decision !== null for facilitators attempting a drop-out, blocking both accepted and rejected decisions. The deferral check was refactored into the same facilitator branch.
  • Frontend (useCourseListRow.tsx): The overflow item condition changed from isPending && decision !== 'Reject' to state === 'upcoming' && decision === null, renaming the action from "Drop out" to "Withdraw application".
  • Tests: Both the component test and the router test were updated to cover the new visibility/permission logic, including explicit cases for null, accepted, and rejected decisions.

Confidence Score: 4/5

Safe to merge — the core withdrawal restriction works correctly in both the frontend and backend.

The backend guard and frontend visibility condition are consistent and correctly implement the intended policy change. The only rough edge is that the FORBIDDEN error message says 'not yet been accepted' but also fires for rejected facilitators (decision = 'Reject'), which is inaccurate if called directly. A test covering the Reject path on the server is also absent. Neither issue affects real users since the UI hides the button for rejected applicants, but they're worth cleaning up.

dropout.ts and dropout.test.ts — the error message wording and the missing 'Reject' test case.

Important Files Changed

Filename Overview
apps/website/src/server/routers/dropout.ts Adds a server-side guard so facilitators can only drop out when decision is null; deferral block moved inside the facilitator branch. Error message is slightly inaccurate for the 'Reject' outcome.
apps/website/src/server/routers/dropout.test.ts Adds tests for accepted-facilitator rejection and null-decision withdrawal; missing coverage for the 'Reject' decision path.
apps/website/src/components/my-courses/useCourseListRow.tsx Replaces the isPending-based condition with state === 'upcoming' && decision === null, renaming the action from 'Drop out' to 'Withdraw application'. Logic is correct and consistent with the new backend guard.
apps/website/src/components/my-courses/CourseListRow.test.tsx Tests updated to match the new 'Withdraw application' label and decision-based visibility logic. New describe block covers shown/hidden states for null, accepted, and rejected decisions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Facilitator calls dropoutOrDeferral] --> B{type === 'Deferral'?}
    B -- Yes --> C[FORBIDDEN: Facilitators cannot defer]
    B -- No --> D{decision !== null?}
    D -- Yes
'Accept' or 'Reject' --> E[FORBIDDEN: Can only withdraw
while still under review]
    D -- No
decision === null --> F[Insert dropout record ✓]

    style C fill:#f88,stroke:#c00
    style E fill:#f88,stroke:#c00
    style F fill:#8f8,stroke:#060
Loading

Comments Outside Diff (1)

  1. apps/website/src/server/routers/dropout.test.ts, line 31-46 (link)

    P2 No test for decision === 'Reject' case on the backend

    Two decision values are covered: null (allowed) and 'Accept' (blocked). The third possible value, 'Reject', is also blocked by decision !== null but has no corresponding test. A future refactor that changes the condition could silently allow a rejected facilitator to call the endpoint without a test catching it.

    Rule Used: Consider adding tests for any new functionality in... (source)

    Learned From
    bluedotimpact/bluedot#956
    bluedotimpact/bluedot#969
    bluedotimpact/bluedot#958

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "[fix] Facilitators can only withdraw an ..." | Re-trigger Greptile

Comment thread apps/website/src/server/routers/dropout.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/website/src/server/routers/dropout.ts (1)

74-76: ⚡ Quick win

Consider clarifying the error message to reflect both rejection and acceptance.

The condition decision !== null blocks withdrawal after both acceptance and rejection, but the error message only mentions acceptance. Consider: "Facilitators can only withdraw an application that is still pending." or "...that has not yet been decided."

💬 Suggested message improvement
       if (courseRegistration.decision !== null) {
-        throw new TRPCError({ code: 'FORBIDDEN', message: 'Facilitators can only withdraw an application that has not yet been accepted.' });
+        throw new TRPCError({ code: 'FORBIDDEN', message: 'Facilitators can only withdraw an application that is still pending.' });
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/server/routers/dropout.ts` around lines 74 - 76, The error
message thrown when checking courseRegistration.decision !== null is misleading
(it only mentions acceptance) — update the TRPCError message created in the
dropout router (the throw new TRPCError({...}) that follows the
courseRegistration.decision !== null check) to clearly state the real condition,
e.g. "Facilitators can only withdraw an application that is still pending." (or
"...that has not yet been decided.") so it covers both rejection and acceptance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/website/src/server/routers/dropout.ts`:
- Around line 74-76: The error message thrown when checking
courseRegistration.decision !== null is misleading (it only mentions acceptance)
— update the TRPCError message created in the dropout router (the throw new
TRPCError({...}) that follows the courseRegistration.decision !== null check) to
clearly state the real condition, e.g. "Facilitators can only withdraw an
application that is still pending." (or "...that has not yet been decided.") so
it covers both rejection and acceptance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e3d367f0-16ef-4084-af64-7cccc640b2ff

📥 Commits

Reviewing files that changed from the base of the PR and between 56ebda7 and 06f9f16.

📒 Files selected for processing (4)
  • apps/website/src/components/my-courses/CourseListRow.test.tsx
  • apps/website/src/components/my-courses/useCourseListRow.tsx
  • apps/website/src/server/routers/dropout.test.ts
  • apps/website/src/server/routers/dropout.ts

@Will-Howard Will-Howard temporarily deployed to wh-2551-facilitator-withdraw-application-2026-05 - bluedot-preview PR #2566 May 31, 2026 10:42 — with Render Destroyed
@Will-Howard Will-Howard temporarily deployed to wh-2551-facilitator-withdraw-application-2026-05 - bluedot-storybook-preview PR #2566 May 31, 2026 10:42 — with Render Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/website/src/components/courses/DropoutModal.tsx (1)

76-90: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate this branch on the registration query finishing.

Until courseRegistrations.getAll resolves, allowDeferral defaults to true and pending applications render the normal drop/defer form before flipping to WithdrawConfirm. On a slow response that can briefly show — and allow interaction with — controls that should never be available for this registration. A short loading state here would avoid the flicker and inconsistent behaviour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/website/src/components/courses/DropoutModal.tsx` around lines 76 - 90,
The branch that returns WithdrawConfirm is executing before
trpc.courseRegistrations.getAll.useQuery() finishes, causing a flicker; update
the component to check the query status (e.g., use the isLoading / isFetching /
isFetched boolean returned from trpc.courseRegistrations.getAll.useQuery()) and
short-circuit render (null or a loading state) until the registrations result is
settled, then compute registration, allowDeferral and isDeferral and evaluate
registration?.decision to return WithdrawConfirm; ensure the gating references
the existing trpc.courseRegistrations.getAll.useQuery(),
registrations/registration, allowDeferral, and WithdrawConfirm symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/website/src/server/routers/dropout.ts`:
- Around line 69-71: The current guard forbids any "Deferral" when
courseRegistration.decision is null, which blocks non-facilitators too; change
the conditional to only apply to facilitators by checking
courseRegistration.role === 'Facilitator' as well (e.g. if (type === 'Deferral'
&& courseRegistration.role === 'Facilitator' && courseRegistration.decision ===
null) throw new TRPCError(...)). Update the guard in the dropout handler where
type and courseRegistration are used.

---

Outside diff comments:
In `@apps/website/src/components/courses/DropoutModal.tsx`:
- Around line 76-90: The branch that returns WithdrawConfirm is executing before
trpc.courseRegistrations.getAll.useQuery() finishes, causing a flicker; update
the component to check the query status (e.g., use the isLoading / isFetching /
isFetched boolean returned from trpc.courseRegistrations.getAll.useQuery()) and
short-circuit render (null or a loading state) until the registrations result is
settled, then compute registration, allowDeferral and isDeferral and evaluate
registration?.decision to return WithdrawConfirm; ensure the gating references
the existing trpc.courseRegistrations.getAll.useQuery(),
registrations/registration, allowDeferral, and WithdrawConfirm symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: feea1056-5d05-4888-b67b-42680c8f6b00

📥 Commits

Reviewing files that changed from the base of the PR and between 06f9f16 and aa114cb.

📒 Files selected for processing (8)
  • apps/website/src/components/courses/DropoutModal.stories.tsx
  • apps/website/src/components/courses/DropoutModal.test.tsx
  • apps/website/src/components/courses/DropoutModal.tsx
  • apps/website/src/components/my-courses/CourseListRow.stories.tsx
  • apps/website/src/components/my-courses/CourseListRow.test.tsx
  • apps/website/src/components/my-courses/useCourseListRow.tsx
  • apps/website/src/server/routers/dropout.test.ts
  • apps/website/src/server/routers/dropout.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/website/src/components/my-courses/CourseListRow.test.tsx

Comment thread apps/website/src/server/routers/dropout.ts
@Will-Howard Will-Howard merged commit 2a32b4c into master May 31, 2026
13 checks passed
@Will-Howard Will-Howard deleted the wh-2551-facilitator-withdraw-application-2026-05 branch May 31, 2026 11:01
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.

Facilitators: Only allow dropping out, not deferring

1 participant