Improved grants AI reviewer#1396
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe PR extends grant application notes from plain text (1000 char limit) to rich HTML content (unlimited via ChangesRich text notes editor with AI formatting and resizable layout
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The PR spans multiple layers: schema/type updates, a new AI formatting utility, rich editor with toolbar components, Notes component refactor with state management changes, ApplicationDetails layout changes with pointer handlers and localStorage, and API validation. Heterogeneous edits across files demand separate reasoning for each checkpoint, but changes follow consistent patterns (HTML conversion, character validation, state synchronization) and the logic density is moderate, not critical. Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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: 5
🧹 Nitpick comments (2)
src/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsx (2)
575-583: ⚡ Quick winConsider adding explicit pointer capture release.
While pointer capture is typically released automatically, adding an
onPointerUphandler to explicitly callreleasePointerCaptureimproves robustness and makes the capture lifecycle explicit.♻️ Proposed addition
Add this handler to the component:
const handleResizePointerUp = useCallback( (event: React.PointerEvent<HTMLButtonElement>) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } }, [], );Then add to the button:
<button aria-label="Resize notes panel" className="group flex w-3 shrink-0 cursor-col-resize items-stretch justify-center self-stretch focus:outline-none" onPointerDown={handleResizePointerDown} onPointerMove={handleResizePointerMove} + onPointerUp={handleResizePointerUp} type="button" >🤖 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 `@src/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsx` around lines 575 - 583, The pointer-capture lifecycle should be made explicit: add a new handler function named handleResizePointerUp (useCallback) that checks event.currentTarget.hasPointerCapture(event.pointerId) and calls event.currentTarget.releasePointerCapture(event.pointerId) when true, and attach it to the resize button as onPointerUp; this complements the existing handleResizePointerDown and handleResizePointerMove handlers and ensures pointer capture is explicitly released.
575-583: 🏗️ Heavy liftConsider adding keyboard support for panel resizing.
The resize handle currently only supports pointer-based interaction. Adding keyboard support (e.g., arrow keys to adjust width) would improve accessibility for users who rely on keyboard navigation.
🤖 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 `@src/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsx` around lines 575 - 583, The resize handle only supports pointer events; add keyboard support by handling key events on the same button element (where handleResizePointerDown and handleResizePointerMove are used): implement an onKeyDown handler (e.g., handleResizeKeyDown) that listens for ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired) to adjust the panel width state or call the same resize logic used by handleResizePointerMove, ensure focus styling and aria attributes (aria-label already present) are preserved, and update any state-updating functions (the resize state updater used by handleResizePointerMove) to be callable from the keyboard handler so keyboard users can incrementally resize the panel.
🤖 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 `@src/app/api/sponsor-dashboard/grant-application/ai/commit-reviewed/route.ts`:
- Around line 87-89: The AI-derived notes are being converted to and stored as
raw HTML via
convertTextToNotesHTML(formatGrantApplicationAiReviewNotes(aiReview)), which
risks stored XSS; update the code so the AI text is sanitized/escaped before
HTML persistence—either modify convertTextToNotesHTML to escape HTML entities
and/or run a strict HTML sanitizer (whitelist-based, e.g., sanitize-html or
DOMPurify server-side) on its output, or escape the result of
formatGrantApplicationAiReviewNotes(aiReview) prior to conversion; ensure the
sanitizer is applied server-side in the route handling (route.ts) and that only
safe tags/attributes are allowed.
- Around line 87-89: The code unconditionally writes notes:
convertTextToNotesHTML(formatGrantApplicationAiReviewNotes(aiReview)) which
becomes an empty string when aiReview is undefined and can wipe existing notes;
change the update logic in route.ts so the notes field is only set when aiReview
is present (e.g., conditionally include notes or set notes to undefined) —
reference the aiReview variable and the functions
formatGrantApplicationAiReviewNotes and convertTextToNotesHTML and ensure
existing notes are preserved by omitting the notes property when aiReview is
falsy.
In `@src/features/grants/types/index.ts`:
- Around line 83-110: The EvaluationResult type uses optional properties (e.g.
recommendation?, confidence?, decisionReason?, risks?, colosseum?,
colosseum.error?, solanaTechnical? and its nested fields, and scores?) which
must be converted to explicit union-with-undefined form; update the
EvaluationResult definition so each optional becomes property: T | undefined
(and for arrays or nested fields use string[] | undefined or error: string |
undefined etc.), preserving the existing types/values but replacing every "?"
optional marker with the explicit "| undefined" form for top-level and nested
fields like colosseum.error and solanaTechnical.* and scores.
In `@src/features/grants/utils/formatGrantApplicationAiReviewNotes.ts`:
- Around line 5-76: Add explicit return type annotations to the top-level helper
arrow functions: labelText should be (value?: string) => string | undefined,
compactText should be (value?: string) => string | undefined, normalizeNote
should be (value?: string) => string | undefined, truncate should be (value:
string, maxLength: number) => string, addMissingRisksToShortNote should be
(note: string, risks?: string[]) => string, and both appendBullet and
appendListBullet should be typed as returning void; update the const
declarations for labelText, compactText, normalizeNote, truncate,
addMissingRisksToShortNote, appendBullet, and appendListBullet with these
explicit return types so TypeScript enforces the intended signatures.
In `@src/features/sponsor-dashboard/components/GrantApplications/Notes.tsx`:
- Around line 108-113: The effect in Notes.tsx only updates local notes when
previousApplicationIdRef.current !== selectedApplication?.id, so when the atom
replaces selectedApplication.notes but keeps the same id the editor stays stale;
modify the useEffect (the one that calls
setNotes(normalizeNotesForEditor(selectedApplication?.notes))) to also detect
changes to notes by adding a previousNotesRef (or comparing
previousNotesRef.current) and update when previousNotesRef.current !==
selectedApplication?.notes (updating previousNotesRef.current after setNotes),
so changes to selectedApplication.notes will sync into the editor even if id is
unchanged.
---
Nitpick comments:
In
`@src/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsx`:
- Around line 575-583: The pointer-capture lifecycle should be made explicit:
add a new handler function named handleResizePointerUp (useCallback) that checks
event.currentTarget.hasPointerCapture(event.pointerId) and calls
event.currentTarget.releasePointerCapture(event.pointerId) when true, and attach
it to the resize button as onPointerUp; this complements the existing
handleResizePointerDown and handleResizePointerMove handlers and ensures pointer
capture is explicitly released.
- Around line 575-583: The resize handle only supports pointer events; add
keyboard support by handling key events on the same button element (where
handleResizePointerDown and handleResizePointerMove are used): implement an
onKeyDown handler (e.g., handleResizeKeyDown) that listens for
ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired) to adjust the
panel width state or call the same resize logic used by handleResizePointerMove,
ensure focus styling and aria attributes (aria-label already present) are
preserved, and update any state-updating functions (the resize state updater
used by handleResizePointerMove) to be callable from the keyboard handler so
keyboard users can incrementally resize the panel.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 434450d5-1f44-428b-9016-808fa4dba202
📒 Files selected for processing (10)
prisma/schema.prismasrc/app/api/sponsor-dashboard/grant-application/ai/commit-reviewed/route.tssrc/features/grants/types/index.tssrc/features/grants/utils/formatGrantApplicationAiReviewNotes.tssrc/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsxsrc/features/sponsor-dashboard/components/GrantApplications/Modals/AiReview.tsxsrc/features/sponsor-dashboard/components/GrantApplications/Notes.tsxsrc/features/sponsor-dashboard/components/NotesRichEditor.tsxsrc/features/sponsor-dashboard/mutations/useCommitReviewsGrantApplications.tssrc/pages/api/sponsor-dashboard/grants/update-notes.ts
| notes: convertTextToNotesHTML( | ||
| formatGrantApplicationAiReviewNotes(aiReview), | ||
| ), |
There was a problem hiding this comment.
Sanitize AI-derived note content before HTML persistence.
convertTextToNotesHTML interpolates content into <li>/<p> without escaping. Since AI output is untrusted text, storing it as raw HTML here creates a stored-XSS risk path.
🤖 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 `@src/app/api/sponsor-dashboard/grant-application/ai/commit-reviewed/route.ts`
around lines 87 - 89, The AI-derived notes are being converted to and stored as
raw HTML via
convertTextToNotesHTML(formatGrantApplicationAiReviewNotes(aiReview)), which
risks stored XSS; update the code so the AI text is sanitized/escaped before
HTML persistence—either modify convertTextToNotesHTML to escape HTML entities
and/or run a strict HTML sanitizer (whitelist-based, e.g., sanitize-html or
DOMPurify server-side) on its output, or escape the result of
formatGrantApplicationAiReviewNotes(aiReview) prior to conversion; ensure the
sanitizer is applied server-side in the route handling (route.ts) and that only
safe tags/attributes are allowed.
Guard against clearing notes when AI review is missing.
This path writes '' into notes when aiReview is undefined, which can erase existing notes for matched applications.
Proposed fix
- notes: convertTextToNotesHTML(
- formatGrantApplicationAiReviewNotes(aiReview),
- ),
+ ...(aiReview
+ ? {
+ notes: convertTextToNotesHTML(
+ formatGrantApplicationAiReviewNotes(aiReview),
+ ),
+ }
+ : {}),🤖 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 `@src/app/api/sponsor-dashboard/grant-application/ai/commit-reviewed/route.ts`
around lines 87 - 89, The code unconditionally writes notes:
convertTextToNotesHTML(formatGrantApplicationAiReviewNotes(aiReview)) which
becomes an empty string when aiReview is undefined and can wipe existing notes;
change the update logic in route.ts so the notes field is only set when aiReview
is present (e.g., conditionally include notes or set notes to undefined) —
reference the aiReview variable and the functions
formatGrantApplicationAiReviewNotes and convertTextToNotesHTML and ensure
existing notes are preserved by omitting the notes property when aiReview is
falsy.
| recommendation?: 'Accept' | 'Reject' | 'Needs_Review'; | ||
| confidence?: 'low' | 'medium' | 'high'; | ||
| reasoning: string; | ||
| decisionReason?: string; | ||
| risks?: string[]; | ||
| colosseum?: { | ||
| enabled: boolean; | ||
| summary: string; | ||
| error?: string; | ||
| }; | ||
| solanaTechnical?: { | ||
| isSolanaTechnical: boolean; | ||
| capabilityAreas: string[]; | ||
| technicalCoherence: 'low' | 'medium' | 'high' | 'not_applicable'; | ||
| missingImplementationDetails: string[]; | ||
| reviewerRisks: string[]; | ||
| summary: string; | ||
| }; | ||
| totalCostInUSD: number; | ||
| totalTimeinMs: number; | ||
| shortNote: string; | ||
| scores?: { | ||
| pow: number; | ||
| activity: number; | ||
| core: number; | ||
| feasibility: number; | ||
| impact: number; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify optional properties in changed EvaluationResult block
rg -n "recommendation\\?:|confidence\\?:|decisionReason\\?:|risks\\?:|colosseum\\?:|solanaTechnical\\?:|scores\\?:" src/features/grants/types/index.tsRepository: SuperteamDAO/earn
Length of output: 280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '60,130p' src/features/grants/types/index.ts | cat -nRepository: SuperteamDAO/earn
Length of output: 1779
🏁 Script executed:
#!/bin/bash
set -euo pipefail
nl -ba src/features/grants/types/index.ts | sed -n '70,120p'Repository: SuperteamDAO/earn
Length of output: 104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n src/features/grants/types/index.ts | sed -n '60,120p'Repository: SuperteamDAO/earn
Length of output: 1779
Replace ?: optional properties with | undefined in EvaluationResult.
type EvaluationResult uses optional props (recommendation?, confidence?, decisionReason?, risks?, colosseum? incl. colosseum.error?, solanaTechnical? incl. ..., and scores?). Switch to property: T | undefined (and for nested optionals, e.g. error: string | undefined) to match the repo TS type-definition rule.
🤖 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 `@src/features/grants/types/index.ts` around lines 83 - 110, The
EvaluationResult type uses optional properties (e.g. recommendation?,
confidence?, decisionReason?, risks?, colosseum?, colosseum.error?,
solanaTechnical? and its nested fields, and scores?) which must be converted to
explicit union-with-undefined form; update the EvaluationResult definition so
each optional becomes property: T | undefined (and for arrays or nested fields
use string[] | undefined or error: string | undefined etc.), preserving the
existing types/values but replacing every "?" optional marker with the explicit
"| undefined" form for top-level and nested fields like colosseum.error and
solanaTechnical.* and scores.
| const labelText = (value?: string) => value?.replace(/_/g, ' '); | ||
|
|
||
| const compactText = (value?: string) => | ||
| value | ||
| ?.replace(/\s+/g, ' ') | ||
| .replace(/^[-*•]\s*/, '') | ||
| .trim(); | ||
|
|
||
| const normalizeNote = (value?: string) => | ||
| value | ||
| ?.split('\n') | ||
| .map((line) => line.replace(/\s+/g, ' ').trim()) | ||
| .filter(Boolean) | ||
| .join('\n') | ||
| .trim(); | ||
|
|
||
| const truncate = (value: string, maxLength: number) => { | ||
| if (value.length <= maxLength) return value; | ||
| return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`; | ||
| }; | ||
|
|
||
| const addMissingRisksToShortNote = (note: string, risks?: string[]) => { | ||
| const compactRisks = risks?.filter(Boolean); | ||
| if (!compactRisks?.length) return note; | ||
|
|
||
| const lowerNote = note.toLowerCase(); | ||
| const alreadyIncluded = compactRisks.every((risk) => | ||
| lowerNote.includes(risk.toLowerCase()), | ||
| ); | ||
|
|
||
| if (alreadyIncluded) return note; | ||
|
|
||
| const riskText = `Calibrated risk: ${compactRisks.join(' ')}`; | ||
| const lines = note.split('\n'); | ||
| const reviewerCheckIndex = lines.findIndex((line) => | ||
| line.toLowerCase().startsWith('reviewer check:'), | ||
| ); | ||
|
|
||
| if (reviewerCheckIndex >= 0) { | ||
| lines[reviewerCheckIndex] = (lines[reviewerCheckIndex] ?? '').replace( | ||
| /^reviewer check:\s*/i, | ||
| `Reviewer check: ${riskText} `, | ||
| ); | ||
| return lines.join('\n'); | ||
| } | ||
|
|
||
| return `${note}\nReviewer check: ${riskText}`; | ||
| }; | ||
|
|
||
| const appendBullet = ( | ||
| lines: string[], | ||
| title: string, | ||
| value?: string, | ||
| maxLength = 220, | ||
| ) => { | ||
| const text = compactText(value); | ||
| if (text) lines.push(`• ${title}: ${truncate(text, maxLength)}`); | ||
| }; | ||
|
|
||
| const appendListBullet = ( | ||
| lines: string[], | ||
| title: string, | ||
| values?: string[], | ||
| maxItems = 2, | ||
| ) => { | ||
| const items = values?.map(compactText).filter(Boolean) as | ||
| | string[] | ||
| | undefined; | ||
| if (items?.length) { | ||
| lines.push(`• ${title}: ${items.slice(0, maxItems).join('; ')}`); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify top-level helper declarations without explicit return types in this file
rg -n "const (labelText|compactText|normalizeNote|truncate|addMissingRisksToShortNote|appendBullet|appendListBullet) = \\(" src/features/grants/utils/formatGrantApplicationAiReviewNotes.tsRepository: SuperteamDAO/earn
Length of output: 407
Add explicit return types to top-level helpers in src/features/grants/utils/formatGrantApplicationAiReviewNotes.ts.
labelText, compactText, normalizeNote, truncate, addMissingRisksToShortNote, appendBullet, and appendListBullet are top-level const arrow functions without return type annotations; add explicit return types to each.
🤖 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 `@src/features/grants/utils/formatGrantApplicationAiReviewNotes.ts` around
lines 5 - 76, Add explicit return type annotations to the top-level helper arrow
functions: labelText should be (value?: string) => string | undefined,
compactText should be (value?: string) => string | undefined, normalizeNote
should be (value?: string) => string | undefined, truncate should be (value:
string, maxLength: number) => string, addMissingRisksToShortNote should be
(note: string, risks?: string[]) => string, and both appendBullet and
appendListBullet should be typed as returning void; update the const
declarations for labelText, compactText, normalizeNote, truncate,
addMissingRisksToShortNote, appendBullet, and appendListBullet with these
explicit return types so TypeScript enforces the intended signatures.
| useEffect(() => { | ||
| if (previousApplicationIdRef.current !== selectedApplication?.id) { | ||
| previousApplicationIdRef.current = selectedApplication?.id; | ||
| setNotes(normalizeNotesForEditor(selectedApplication?.notes)); | ||
| } | ||
| if (value.length <= MAX_CHARACTERS) { | ||
| }, [selectedApplication?.id, selectedApplication?.notes]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether selected grant application notes are updated for the same id in other flows.
# Expected: find same-id mutations to selectedGrantApplicationAtom that can change `notes`.
rg -n -C3 "setSelectedApplication\\(|selectedGrantApplicationAtom|getTextCharacterCount\\(|notes:" --type=tsx --type=tsRepository: SuperteamDAO/earn
Length of output: 89
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the effect in the reviewed component
sed -n '80,150p' src/features/sponsor-dashboard/components/GrantApplications/Notes.tsx
# 2) Locate the state/atom backing `selectedApplication` and any setters
rg -n --glob='*.ts' --glob='*.tsx' -S "selectedGrantApplicationAtom|selectedApplicationAtom|setSelectedApplication|selectedApplication" src/features/sponsor-dashboard/components/GrantApplications
rg -n --glob='*.ts' --glob='*.tsx' -S "selectedGrantApplicationAtom|setSelectedApplication" src/features/sponsor-dashboard
# 3) Find other places that update notes on the same application record
rg -n --glob='*.ts' --glob='*.tsx' -S "notes\\s*:" src/features/sponsor-dashboard
rg -n --glob='*.ts' --glob='*.tsx' -S "set.*notes|update.*notes|normalizeNotesForEditor\\(" src/features/sponsor-dashboard
# 4) Search broadly for updates to the selected grant application object (including notes)
rg -n --glob='*.ts' --glob='*.tsx' -S "(selectedGrantApplication|selectedApplication)\\s*.*notes" src/features/sponsor-dashboard
# 5) If atoms live elsewhere, broaden search (still scoped)
rg -n --glob='*.ts' --glob='*.tsx' -S "selectedGrantApplicationAtom|setSelectedApplication|GrantApplication.*Atom|selectedApplication" srcRepository: SuperteamDAO/earn
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact useEffect + surrounding state/refs
sed -n '1,170p' src/features/sponsor-dashboard/components/GrantApplications/Notes.tsx
# Show selectedGrantApplicationAtom implementation
sed -n '1,140p' src/features/sponsor-dashboard/atoms/index.ts
# Show commit reviews mutation where notes may be updated for an existing application id
sed -n '1,140p' src/features/sponsor-dashboard/mutations/useCommitReviewsGrantApplications.ts
# Show any other mutations/components that update selected application object
rg -n --glob='*.ts' --glob='*.tsx' -S "setSelectedApplication\\(" src/features/sponsor-dashboard/mutations src/features/sponsor-dashboard/components/GrantApplications
# Specifically show where notes are updated in GrantApplications area
rg -n --glob='*.ts' --glob='*.tsx' -S "notes\\s*:" src/features/sponsor-dashboard/mutations/useCommitReviewsGrantApplications.ts src/features/sponsor-dashboard/components/GrantApplications/Notes.tsx src/features/sponsor-dashboard/components/GrantApplications/ApplicationDetails.tsxRepository: SuperteamDAO/earn
Length of output: 12270
Sync Notes editor when selectedApplication.notes changes for the same application ID.
Notes.tsx only updates local notes when previousApplicationIdRef.current !== selectedApplication?.id (Lines 109-113). However useCommitReviewsGrantApplications updates selectedGrantApplicationAtom by replacing notes while keeping the same application object shape (so id can remain unchanged), causing the editor to display stale notes.
Proposed fix
- const previousApplicationIdRef = useRef<string | undefined>(
- selectedApplication?.id,
- );
@@
useEffect(() => {
- if (previousApplicationIdRef.current !== selectedApplication?.id) {
- previousApplicationIdRef.current = selectedApplication?.id;
- setNotes(normalizeNotesForEditor(selectedApplication?.notes));
- }
- }, [selectedApplication?.id, selectedApplication?.notes]);
+ const normalized = normalizeNotesForEditor(selectedApplication?.notes) || '';
+ setNotes((prev) => (prev === normalized ? prev : normalized));
+ }, [selectedApplication?.id, selectedApplication?.notes]);🤖 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 `@src/features/sponsor-dashboard/components/GrantApplications/Notes.tsx` around
lines 108 - 113, The effect in Notes.tsx only updates local notes when
previousApplicationIdRef.current !== selectedApplication?.id, so when the atom
replaces selectedApplication.notes but keeps the same id the editor stays stale;
modify the useEffect (the one that calls
setNotes(normalizeNotesForEditor(selectedApplication?.notes))) to also detect
changes to notes by adding a previousNotesRef (or comparing
previousNotesRef.current) and update when previousNotesRef.current !==
selectedApplication?.notes (updating previousNotesRef.current after setNotes),
so changes to selectedApplication.notes will sync into the editor even if id is
unchanged.
What does this PR do?
Where should the reviewer start?
How should this be manually tested?
Any background context you want to provide?
What are the relevant issues?
Screenshots (if appropriate)
Summary by CodeRabbit
Release Notes