feat: implement submission draft system#119
Conversation
|
@devJaja is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a submission-draft system: new types, a localStorage-backed Changes
Sequence DiagramsequenceDiagram
actor User
participant Component as BountyDetailSubmissionsCard
participant Hook as useSubmissionDraft
participant Storage as localStorage
participant API as SubmissionAPI
User->>Component: Open submit PR dialog
Component->>Hook: initialize(bountyId)
Hook->>Storage: load draft for bountyId
Storage-->>Hook: return draft (if present)
Hook-->>Component: provide draft state
Component->>Component: restore fields & show "Draft restored"
User->>Component: Edit PR URL / comments
Component->>Hook: autoSave(formData)
Note over Hook: debounce 1s, save only if fields present
Hook->>Storage: persist draft with updatedAt
Storage-->>Hook: confirm save
User->>Component: Submit form
Component->>API: submit
API-->>Component: success
Component->>Hook: clearDraft()
Hook->>Storage: remove draft
Storage-->>Hook: confirm clear
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
hooks/__tests__/use-submission-draft.test.ts (1)
52-69: Use fake timers for deterministic debounce testing.The 1.5s real-time
waitForwindow makes this test slower and potentially flaky under load. Advance fake timers instead: setupjest.useFakeTimers()before the test, calljest.advanceTimersByTime(AUTO_SAVE_DELAY)afterautoSave(), and assert immediately.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/__tests__/use-submission-draft.test.ts` around lines 52 - 69, The test currently relies on a real wait window; make it deterministic by using Jest fake timers: in the "should auto-save after delay" test enable fake timers with jest.useFakeTimers(), call result.current.autoSave(formData) and then advance timers by the debounce constant (AUTO_SAVE_DELAY) via jest.advanceTimersByTime(AUTO_SAVE_DELAY), then assert result.current.draft?.formData equals formData immediately (remove the long waitFor). Ensure to restore timers after the test if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/bounty-detail/bounty-detail-submissions-card.tsx`:
- Around line 69-81: The auto-save runs immediately after the draft hydration
and overwrites updatedAt; to fix, add a hydration guard (e.g., a ref or state
like isHydrated) that is set true inside the draft-useEffect after
setPrUrl/setSubmitComments, and change the auto-save useEffect to return early
(or not call autoSave) when isHydrated is false so autosave only starts after
the initial draft values are loaded; reference the existing symbols draft,
setPrUrl, setSubmitComments, prUrl, submitComments, and autoSave to implement
this guard.
In `@docs/SUBMISSION_DRAFTS_EXAMPLE.tsx`:
- Around line 35-49: The handleSubmit async function is missing
event.preventDefault(), so update handleSubmit to accept the form event (e.g.,
event: React.FormEvent) and immediately call event.preventDefault() before
awaiting submitToAPI({ prUrl, comments }); to prevent the browser from doing a
full page submit and interrupting the async flow; keep the existing
clearDraft(), setPrUrl(""), and setComments("") logic after the await so the
draft reset still occurs on success.
In `@hooks/use-submission-draft.ts`:
- Around line 29-38: The autoSave callback currently skips saving when both
formData.githubPullRequestUrl and formData.comments are empty, leaving stale
drafts; update the autoSave logic (in use-submission-draft.ts -> autoSave) to
always call saveDraft(formData) after the debounce (or call a dedicated
clearDraft when available) instead of only when those fields are present so an
empty form clears the persisted draft; keep the same AUTO_SAVE_DELAY and return
the clearTimeout cleanup as before.
---
Nitpick comments:
In `@hooks/__tests__/use-submission-draft.test.ts`:
- Around line 52-69: The test currently relies on a real wait window; make it
deterministic by using Jest fake timers: in the "should auto-save after delay"
test enable fake timers with jest.useFakeTimers(), call
result.current.autoSave(formData) and then advance timers by the debounce
constant (AUTO_SAVE_DELAY) via jest.advanceTimersByTime(AUTO_SAVE_DELAY), then
assert result.current.draft?.formData equals formData immediately (remove the
long waitFor). Ensure to restore timers after the test if needed.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
components/bounty-detail/bounty-detail-submissions-card.tsxdocs/SUBMISSION_DRAFTS_EXAMPLE.tsxhooks/__tests__/use-submission-draft.test.tshooks/use-submission-draft.tstypes/submission-draft.ts
| const handleSubmit = async () => { | ||
| // Your submit logic here | ||
| await submitToAPI({ prUrl, comments }); | ||
|
|
||
| // Clear draft after success | ||
| clearDraft(); | ||
|
|
||
| // Reset form | ||
| setPrUrl(""); | ||
| setComments(""); | ||
| }; | ||
|
|
||
| return ( | ||
| <form onSubmit={handleSubmit}> | ||
| {/* Show draft indicator */} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "SUBMISSION_DRAFTS_EXAMPLE.tsx"Repository: boundlessfi/bounties
Length of output: 98
🏁 Script executed:
cat -n docs/SUBMISSION_DRAFTS_EXAMPLE.tsxRepository: boundlessfi/bounties
Length of output: 2523
Add preventDefault() to the async form submit handler to prevent page reload.
The form handler is missing event.preventDefault(), which allows the browser's default form submission to interrupt the async submitToAPI call and draft system flow.
Required changes
-import { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
+import type { FormEvent } from "react";
- const handleSubmit = async () => {
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
// Your submit logic here
await submitToAPI({ prUrl, comments });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/SUBMISSION_DRAFTS_EXAMPLE.tsx` around lines 35 - 49, The handleSubmit
async function is missing event.preventDefault(), so update handleSubmit to
accept the form event (e.g., event: React.FormEvent) and immediately call
event.preventDefault() before awaiting submitToAPI({ prUrl, comments }); to
prevent the browser from doing a full page submit and interrupting the async
flow; keep the existing clearDraft(), setPrUrl(""), and setComments("") logic
after the await so the draft reset still occurs on success.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| const autoSave = useCallback( | ||
| (formData: SubmissionForm) => { | ||
| const timer = setTimeout(() => { | ||
| if (formData.githubPullRequestUrl || formData.comments) { | ||
| saveDraft(formData); | ||
| } | ||
| }, AUTO_SAVE_DELAY); | ||
| return () => clearTimeout(timer); | ||
| }, | ||
| [saveDraft] |
There was a problem hiding this comment.
Clear persisted draft when form content becomes empty.
At Line 32, empty form state is ignored rather than persisted/cleared, so stale draft data can be restored later.
💡 Proposed fix
const autoSave = useCallback(
(formData: SubmissionForm) => {
const timer = setTimeout(() => {
- if (formData.githubPullRequestUrl || formData.comments) {
+ const hasContent =
+ formData.githubPullRequestUrl.trim().length > 0 ||
+ formData.comments.trim().length > 0;
+
+ if (hasContent) {
saveDraft(formData);
+ } else {
+ clearDraft();
}
}, AUTO_SAVE_DELAY);
return () => clearTimeout(timer);
},
- [saveDraft]
+ [saveDraft, clearDraft]
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/use-submission-draft.ts` around lines 29 - 38, The autoSave callback
currently skips saving when both formData.githubPullRequestUrl and
formData.comments are empty, leaving stale drafts; update the autoSave logic (in
use-submission-draft.ts -> autoSave) to always call saveDraft(formData) after
the debounce (or call a dedicated clearDraft when available) instead of only
when those fields are present so an empty form clears the persisted draft; keep
the same AUTO_SAVE_DELAY and return the clearTimeout cleanup as before.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
|
@devJaja Resolve coderabbit comments and also. thhe conflict |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
components/bounty-detail/bounty-detail-submissions-card.tsx (1)
69-81:⚠️ Potential issue | 🟠 MajorSkip the first auto-save cycle after draft hydration (still unresolved).
Line 69-75 hydrates
prUrl/submitComments, then Line 77-81 immediately schedules auto-save, which rewritesupdatedAtwithout user input and can mislead the restored timestamp UI.💡 Proposed fix
-import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; @@ const [prUrl, setPrUrl] = useState(""); - const [submitComments, setSubmitComments] = useState(""); - const [reviewComments, setReviewComments] = useState(""); + const [submitComments, setSubmitComments] = useState(""); + const [reviewComments, setReviewComments] = useState(""); + const skipNextAutoSaveRef = useRef(false); @@ useEffect(() => { if (draft?.formData) { setPrUrl(draft.formData.githubPullRequestUrl); setSubmitComments(draft.formData.comments); + skipNextAutoSaveRef.current = true; } }, [draft]); @@ useEffect(() => { + if (skipNextAutoSaveRef.current) { + skipNextAutoSaveRef.current = false; + return; + } const cleanup = autoSave({ githubPullRequestUrl: prUrl, comments: submitComments }); return cleanup; }, [prUrl, submitComments, autoSave]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-submissions-card.tsx` around lines 69 - 81, The current auto-save effect immediately runs after draft hydration and overwrites updatedAt; prevent that by skipping the first auto-save run triggered by the hydration effect: add a hydration flag/ref (e.g., hasHydratedDraft) that you set to true at the end of the draft hydration useEffect (the block that calls setPrUrl/setSubmitComments) and then guard the autoSave effect to no-op unless hasHydratedDraft is true (use a ref or state checked inside the useEffect that calls autoSave). This keeps symbols clear: set hasHydratedDraft after the draft useEffect finishes, and only call autoSave({ githubPullRequestUrl: prUrl, comments: submitComments }) from the auto-save useEffect when hasHydratedDraft is true to avoid the unwanted initial save.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@components/bounty-detail/bounty-detail-submissions-card.tsx`:
- Around line 69-81: The current auto-save effect immediately runs after draft
hydration and overwrites updatedAt; prevent that by skipping the first auto-save
run triggered by the hydration effect: add a hydration flag/ref (e.g.,
hasHydratedDraft) that you set to true at the end of the draft hydration
useEffect (the block that calls setPrUrl/setSubmitComments) and then guard the
autoSave effect to no-op unless hasHydratedDraft is true (use a ref or state
checked inside the useEffect that calls autoSave). This keeps symbols clear: set
hasHydratedDraft after the draft useEffect finishes, and only call autoSave({
githubPullRequestUrl: prUrl, comments: submitComments }) from the auto-save
useEffect when hasHydratedDraft is true to avoid the unwanted initial save.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
components/bounty-detail/bounty-detail-submissions-card.tsx
@0xdevcollins Done |
|
@devJaja |
|
@devJaja |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
components/bounty-detail/bounty-detail-submissions-card.tsx (1)
69-85:⚠️ Potential issue | 🟠 MajorHydration guard still allows one immediate re-save of restored values.
This still updates
updatedAtright after restore (no user edit): hydration sets state, then the next autosave run persists the same values. Add a one-shot skip flag for the first autosave after hydration.💡 Proposed fix
- const hasHydratedDraft = useRef(false); + const hasHydratedDraft = useRef(false); + const skipNextAutoSaveRef = useRef(false); // Load draft on mount useEffect(() => { if (draft?.formData) { setPrUrl(draft.formData.githubPullRequestUrl); setSubmitComments(draft.formData.comments); + skipNextAutoSaveRef.current = true; } hasHydratedDraft.current = true; }, [draft]); // Auto-save on form changes useEffect(() => { if (!hasHydratedDraft.current) return; + if (skipNextAutoSaveRef.current) { + skipNextAutoSaveRef.current = false; + return; + } const cleanup = autoSave({ githubPullRequestUrl: prUrl, comments: submitComments }); return cleanup; }, [prUrl, submitComments, autoSave]);Please verify by restoring an existing draft and waiting >1s without typing; the displayed draft timestamp should remain unchanged.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-submissions-card.tsx` around lines 69 - 85, The hydration guard currently sets hasHydratedDraft.current = true immediately, allowing the very next autosave to persist restored values; modify the logic to add a one-shot skip flag (e.g., skipNextAutosaveRef) that is set to true when you hydrate the draft (inside the first useEffect where draft is applied) and checked in the autosave useEffect before calling autoSave; if skipNextAutosaveRef.current is true, clear it and return without saving so the first autosave after hydration is skipped, otherwise call autoSave({ githubPullRequestUrl: prUrl, comments: submitComments }) as before. Ensure you reference hasHydratedDraft, skipNextAutosaveRef (new), autoSave, prUrl, submitComments and draft in the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/bounty-detail/bounty-detail-submissions-card.tsx`:
- Line 48: The draft storage is currently only keyed by bounty id, allowing
drafts to leak between accounts; update the draft hook and its usage so drafts
are scoped by both user id and bounty id. Change the call site that currently
does useSubmissionDraft(bounty.id) (and references draft, clearDraft, autoSave)
to pass the current user's stable id as well (e.g.,
useSubmissionDraft(bounty.id, userId)), and modify the useSubmissionDraft
implementation to incorporate userId into the persistence key and to include a
canonical bountyId and userId in the stored payload so restore/clear operations
validate both identifiers.
---
Duplicate comments:
In `@components/bounty-detail/bounty-detail-submissions-card.tsx`:
- Around line 69-85: The hydration guard currently sets hasHydratedDraft.current
= true immediately, allowing the very next autosave to persist restored values;
modify the logic to add a one-shot skip flag (e.g., skipNextAutosaveRef) that is
set to true when you hydrate the draft (inside the first useEffect where draft
is applied) and checked in the autosave useEffect before calling autoSave; if
skipNextAutosaveRef.current is true, clear it and return without saving so the
first autosave after hydration is skipped, otherwise call autoSave({
githubPullRequestUrl: prUrl, comments: submitComments }) as before. Ensure you
reference hasHydratedDraft, skipNextAutosaveRef (new), autoSave, prUrl,
submitComments and draft in the change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
components/bounty-detail/bounty-detail-submissions-card.tsx
| }: BountyDetailSubmissionsCardProps) { | ||
| const { data: session } = authClient.useSession(); | ||
| const submissions = bounty.submissions || []; | ||
| const { draft, clearDraft, autoSave } = useSubmissionDraft(bounty.id); |
There was a problem hiding this comment.
Scope draft storage by user identity to prevent cross-account draft exposure.
useSubmissionDraft(bounty.id) scopes persisted data only by bounty. On shared browsers, a different signed-in user visiting the same bounty can restore someone else’s draft content. Please key drafts by both user id and bounty id (likely by extending the hook API so bountyId in the stored payload remains canonical).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/bounty-detail/bounty-detail-submissions-card.tsx` at line 48, The
draft storage is currently only keyed by bounty id, allowing drafts to leak
between accounts; update the draft hook and its usage so drafts are scoped by
both user id and bounty id. Change the call site that currently does
useSubmissionDraft(bounty.id) (and references draft, clearDraft, autoSave) to
pass the current user's stable id as well (e.g., useSubmissionDraft(bounty.id,
userId)), and modify the useSubmissionDraft implementation to incorporate userId
into the persistence key and to include a canonical bountyId and userId in the
stored payload so restore/clear operations validate both identifiers.
There was a problem hiding this comment.
⚠️ Potential issue | 🟠 MajorScope draft storage by user identity to prevent cross-account draft exposure.
useSubmissionDraft(bounty.id)scopes persisted data only by bounty. On shared browsers, a different signed-in user visiting the same bounty can restore someone else’s draft content. Please key drafts by both user id and bounty id (likely by extending the hook API sobountyIdin the stored payload remains canonical).🤖 Prompt for AI Agents
skip it
There was a problem hiding this comment.
@devJaja, understood – I'll skip this suggestion. Feel free to revisit it later if cross-user draft isolation becomes a concern.
/)_/)
(•_• )
/>💭 (okay...)
Implement Submission Draft System
Summary
Added auto-save draft functionality for bounty submissions. Users can now save unfinished submissions that
automatically restore when they return.
Changes
Features
Testing
bash
npm test use-submission-draft
Acceptance Criteria
Closes: #82
Summary by CodeRabbit