@@ -399,66 +399,204 @@ export const PROverview = memo(function PROverview() {
399399 await store . restoreBranch ( ) ;
400400 } , [ store ] ) ;
401401
402+ // Helper to refetch timeline after mutations
403+ const refetchTimeline = useCallback ( ( ) => {
404+ github
405+ . getPRTimeline ( owner , repo , pr . number )
406+ . then ( ( timeline ) => store . setTimeline ( timeline ) )
407+ . catch ( ( ) => { } ) ;
408+ } , [ github , owner , repo , pr . number , store ] ) ;
409+
402410 const handleRequestReviewer = useCallback (
403411 async ( login : string ) => {
412+ // Find the collaborator to get avatar_url
413+ const collaborator = collaborators . find ( ( c ) => c . login === login ) ;
404414 try {
415+ // 1. Request GitHub to add reviewer
405416 await github . requestReviewers ( owner , repo , pr . number , [ login ] ) ;
406- await refetchPR ( ) ;
417+
418+ // 2. Update our state with the known change
419+ const newReviewer = {
420+ login,
421+ avatar_url : collaborator ?. avatar_url ?? "" ,
422+ id : 0 ,
423+ node_id : "" ,
424+ gravatar_id : "" ,
425+ url : "" ,
426+ html_url : "" ,
427+ followers_url : "" ,
428+ following_url : "" ,
429+ gists_url : "" ,
430+ starred_url : "" ,
431+ subscriptions_url : "" ,
432+ organizations_url : "" ,
433+ repos_url : "" ,
434+ events_url : "" ,
435+ received_events_url : "" ,
436+ type : "User" as const ,
437+ site_admin : false ,
438+ user_view_type : "public" as const ,
439+ } ;
440+ store . setPr ( {
441+ ...pr ,
442+ requested_reviewers : [ ...( pr . requested_reviewers ?? [ ] ) , newReviewer ] ,
443+ } ) ;
444+
445+ // 3. Invalidate cache so future fetches get fresh data
446+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
447+
448+ // 4. Refetch timeline (reviewer request creates event)
449+ refetchTimeline ( ) ;
407450 } catch ( error ) {
408451 console . error ( "Failed to request reviewer:" , error ) ;
409452 }
410453 } ,
411- [ github , owner , repo , pr . number , refetchPR ]
454+ [ github , owner , repo , pr , store , collaborators , refetchTimeline ]
412455 ) ;
413456
414457 const handleRemoveReviewer = useCallback (
415458 async ( login : string ) => {
416459 try {
460+ // 1. Request GitHub to remove reviewer
417461 await github . removeReviewers ( owner , repo , pr . number , [ login ] ) ;
418- await refetchPR ( ) ;
462+
463+ // 2. Update our state with the known change
464+ store . setPr ( {
465+ ...pr ,
466+ requested_reviewers : ( pr . requested_reviewers ?? [ ] ) . filter (
467+ ( r ) => r . login !== login
468+ ) ,
469+ } ) ;
470+
471+ // 3. Invalidate cache so future fetches get fresh data
472+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
473+
474+ // 4. Refetch timeline
475+ refetchTimeline ( ) ;
419476 } catch ( error ) {
420477 console . error ( "Failed to remove reviewer:" , error ) ;
421478 }
422479 } ,
423- [ github , owner , repo , pr . number , refetchPR ]
480+ [ github , owner , repo , pr , store , refetchTimeline ]
424481 ) ;
425482
426483 const handleAddAssignee = useCallback (
427484 async ( login : string ) => {
485+ // Find the collaborator to get avatar_url
486+ const collaborator = collaborators . find ( ( c ) => c . login === login ) ;
428487 try {
488+ // 1. Request GitHub to add assignee
429489 await github . addAssignees ( owner , repo , pr . number , [ login ] ) ;
430- await refetchPR ( ) ;
490+
491+ // 2. Update our state with the known change
492+ const newAssignee = {
493+ login,
494+ avatar_url : collaborator ?. avatar_url ?? "" ,
495+ id : 0 ,
496+ node_id : "" ,
497+ gravatar_id : "" ,
498+ url : "" ,
499+ html_url : "" ,
500+ followers_url : "" ,
501+ following_url : "" ,
502+ gists_url : "" ,
503+ starred_url : "" ,
504+ subscriptions_url : "" ,
505+ organizations_url : "" ,
506+ repos_url : "" ,
507+ events_url : "" ,
508+ received_events_url : "" ,
509+ type : "User" as const ,
510+ site_admin : false ,
511+ user_view_type : "public" as const ,
512+ } ;
513+ store . setPr ( {
514+ ...pr ,
515+ assignees : [ ...( pr . assignees ?? [ ] ) , newAssignee ] ,
516+ } ) ;
517+
518+ // 3. Invalidate cache so future fetches get fresh data
519+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
520+
521+ // 4. Refetch timeline (assignee change creates event)
522+ refetchTimeline ( ) ;
431523 } catch ( error ) {
432524 console . error ( "Failed to add assignee:" , error ) ;
433525 }
434526 } ,
435- [ github , owner , repo , pr . number , refetchPR ]
527+ [ github , owner , repo , pr , store , collaborators , refetchTimeline ]
436528 ) ;
437529
438530 const handleRemoveAssignee = useCallback (
439531 async ( login : string ) => {
440532 try {
533+ // 1. Request GitHub to remove assignee
441534 await github . removeAssignees ( owner , repo , pr . number , [ login ] ) ;
442- await refetchPR ( ) ;
535+
536+ // 2. Update our state with the known change
537+ store . setPr ( {
538+ ...pr ,
539+ assignees : ( pr . assignees ?? [ ] ) . filter ( ( a ) => a . login !== login ) ,
540+ } ) ;
541+
542+ // 3. Invalidate cache so future fetches get fresh data
543+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
544+
545+ // 4. Refetch timeline
546+ refetchTimeline ( ) ;
443547 } catch ( error ) {
444548 console . error ( "Failed to remove assignee:" , error ) ;
445549 }
446550 } ,
447- [ github , owner , repo , pr . number , refetchPR ]
551+ [ github , owner , repo , pr , store , refetchTimeline ]
448552 ) ;
449553
450554 const handleAssignSelf = useCallback ( async ( ) => {
451555 if ( ! currentUser ) return ;
556+
452557 setAssigningSelf ( true ) ;
453558 try {
559+ // 1. Request GitHub to add assignee
454560 await github . addAssignees ( owner , repo , pr . number , [ currentUser ] ) ;
455- await refetchPR ( ) ;
561+
562+ // 2. Update our state with the known change
563+ const newAssignee = {
564+ login : currentUser ,
565+ avatar_url : "" ,
566+ id : 0 ,
567+ node_id : "" ,
568+ gravatar_id : "" ,
569+ url : "" ,
570+ html_url : "" ,
571+ followers_url : "" ,
572+ following_url : "" ,
573+ gists_url : "" ,
574+ starred_url : "" ,
575+ subscriptions_url : "" ,
576+ organizations_url : "" ,
577+ repos_url : "" ,
578+ events_url : "" ,
579+ received_events_url : "" ,
580+ type : "User" as const ,
581+ site_admin : false ,
582+ user_view_type : "public" as const ,
583+ } ;
584+ store . setPr ( {
585+ ...pr ,
586+ assignees : [ ...( pr . assignees ?? [ ] ) , newAssignee ] ,
587+ } ) ;
588+
589+ // 3. Invalidate cache so future fetches get fresh data
590+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
591+
592+ // 4. Refetch timeline
593+ refetchTimeline ( ) ;
456594 } catch ( error ) {
457595 console . error ( "Failed to assign self:" , error ) ;
458596 } finally {
459597 setAssigningSelf ( false ) ;
460598 }
461- } , [ github , owner , repo , pr . number , currentUser , refetchPR ] ) ;
599+ } , [ github , owner , repo , pr , currentUser , store , refetchTimeline ] ) ;
462600
463601 // Reaction state - keyed by "issue" for PR body or comment ID
464602 const [ reactions , setReactions ] = useState < Record < string , Reaction [ ] > > ( { } ) ;
@@ -1639,7 +1777,44 @@ export const PROverview = memo(function PROverview() {
16391777 pr = { pr }
16401778 owner = { owner }
16411779 repo = { repo }
1642- onUpdate = { refetchPR }
1780+ onLabelToggle = { async ( labelName , labelColor , hasLabel ) => {
1781+ try {
1782+ // 1. Request GitHub to toggle label
1783+ if ( hasLabel ) {
1784+ await github . removeLabel ( owner , repo , pr . number , labelName ) ;
1785+ } else {
1786+ await github . addLabels ( owner , repo , pr . number , [ labelName ] ) ;
1787+ }
1788+
1789+ // 2. Update our state with the known change
1790+ const newLabels = hasLabel
1791+ ? pr . labels . filter ( ( l ) => l . name !== labelName )
1792+ : [
1793+ ...pr . labels ,
1794+ {
1795+ id : 0 ,
1796+ node_id : "" ,
1797+ url : "" ,
1798+ name : labelName ,
1799+ color : labelColor ,
1800+ default : false ,
1801+ description : null ,
1802+ } ,
1803+ ] ;
1804+ store . setPr ( { ...pr , labels : newLabels } ) ;
1805+
1806+ // 3. Invalidate cache so future fetches get fresh data
1807+ github . invalidateCache ( `pr:${ owner } /${ repo } /${ pr . number } ` ) ;
1808+
1809+ // 4. Refetch timeline (label change creates event)
1810+ github
1811+ . getPRTimeline ( owner , repo , pr . number )
1812+ . then ( ( timeline ) => store . setTimeline ( timeline ) )
1813+ . catch ( ( ) => { } ) ;
1814+ } catch ( error ) {
1815+ console . error ( "Failed to toggle label:" , error ) ;
1816+ }
1817+ } }
16431818 canWrite = { canMergeRepo }
16441819 />
16451820
@@ -1754,23 +1929,22 @@ function SidebarSection({
17541929// ============================================================================
17551930
17561931interface LabelsSectionProps {
1757- pr : {
1758- labels : Array < { name : string ; color : string } > ;
1759- state : string ;
1760- merged ?: boolean ;
1761- number : number ;
1762- } ;
1932+ pr : PullRequest ;
17631933 owner : string ;
17641934 repo : string ;
1765- onUpdate : ( ) => Promise < void > ;
1935+ onLabelToggle : (
1936+ labelName : string ,
1937+ labelColor : string ,
1938+ hasLabel : boolean
1939+ ) => Promise < void > ;
17661940 canWrite ?: boolean ;
17671941}
17681942
17691943function LabelsSection ( {
17701944 pr,
17711945 owner,
17721946 repo,
1773- onUpdate ,
1947+ onLabelToggle ,
17741948 canWrite = true ,
17751949} : LabelsSectionProps ) {
17761950 const github = useGitHub ( ) ;
@@ -1808,20 +1982,11 @@ function LabelsSection({
18081982 } , [ showPicker , fetchLabels ] ) ;
18091983
18101984 const handleToggleLabel = useCallback (
1811- async ( labelName : string ) => {
1812- try {
1813- const hasLabel = pr . labels . some ( ( l ) => l . name === labelName ) ;
1814- if ( hasLabel ) {
1815- await github . removeLabel ( owner , repo , pr . number , labelName ) ;
1816- } else {
1817- await github . addLabels ( owner , repo , pr . number , [ labelName ] ) ;
1818- }
1819- await onUpdate ( ) ;
1820- } catch ( error ) {
1821- console . error ( "Failed to toggle label:" , error ) ;
1822- }
1985+ async ( labelName : string , labelColor : string ) => {
1986+ const hasLabel = pr . labels . some ( ( l ) => l . name === labelName ) ;
1987+ await onLabelToggle ( labelName , labelColor , hasLabel ) ;
18231988 } ,
1824- [ github , owner , repo , pr . number , pr . labels , onUpdate ]
1989+ [ pr . labels , onLabelToggle ]
18251990 ) ;
18261991
18271992 const canEdit = canWrite && pr . state === "open" && ! pr . merged ;
@@ -1889,7 +2054,7 @@ function LabelsSection({
18892054 return (
18902055 < button
18912056 key = { label . name }
1892- onClick = { ( ) => handleToggleLabel ( label . name ) }
2057+ onClick = { ( ) => handleToggleLabel ( label . name , label . color ) }
18932058 className = "w-full flex items-center gap-2 px-3 py-2 hover:bg-muted transition-colors text-left"
18942059 >
18952060 < div className = "w-4 h-4 flex items-center justify-center" >
0 commit comments