1- import { GitHubTrackerSettings , ProjectData , ProjectFieldValue , ProjectInfo } from "./types" ;
1+ import { GitHubTrackerSettings , ProjectData , ProjectFieldValue , ProjectInfo , ProjectStatusOption } from "./types" ;
22import { Octokit } from "octokit" ;
33import { NoticeManager } from "./notice-manager" ;
44import {
88 GET_ORGANIZATION_PROJECTS ,
99 GET_USER_PROJECTS ,
1010 GET_PROJECT_ITEMS ,
11+ GET_PROJECT_FIELDS ,
1112 parseItemProjectData ,
1213 ProjectItemData ,
1314} from "./github-graphql" ;
@@ -689,6 +690,130 @@ export class GitHubClient {
689690 } ) ;
690691 }
691692
693+ /**
694+ * Fetch all available projects for the authenticated user (user + org projects)
695+ */
696+ public async fetchAllAvailableProjects ( ) : Promise < ProjectInfo [ ] > {
697+ if ( ! this . octokit ) {
698+ return [ ] ;
699+ }
700+
701+ const projects : ProjectInfo [ ] = [ ] ;
702+ const seenIds = new Set < string > ( ) ;
703+
704+ try {
705+ // Get authenticated user
706+ const user = await this . fetchAuthenticatedUser ( ) ;
707+ if ( ! user ) {
708+ this . noticeManager . error ( "Could not get authenticated user" ) ;
709+ return [ ] ;
710+ }
711+
712+ // Fetch user's own projects
713+ try {
714+ let hasNextPage = true ;
715+ let cursor : string | null = null ;
716+
717+ while ( hasNextPage ) {
718+ const userResponse : any = await this . octokit . graphql (
719+ GET_USER_PROJECTS ,
720+ {
721+ user : user ,
722+ first : 50 ,
723+ after : cursor ,
724+ } ,
725+ ) ;
726+
727+ if ( userResponse ?. user ?. projectsV2 ?. nodes ) {
728+ for ( const node of userResponse . user . projectsV2 . nodes ) {
729+ if ( ! seenIds . has ( node . id ) ) {
730+ seenIds . add ( node . id ) ;
731+ projects . push ( {
732+ id : node . id ,
733+ title : node . title ,
734+ number : node . number ,
735+ url : node . url ,
736+ closed : node . closed ,
737+ owner : user ,
738+ } ) ;
739+ }
740+ }
741+ }
742+
743+ hasNextPage = userResponse ?. user ?. projectsV2 ?. pageInfo ?. hasNextPage ?? false ;
744+ cursor = userResponse ?. user ?. projectsV2 ?. pageInfo ?. endCursor ?? null ;
745+ }
746+ } catch ( error ) {
747+ this . noticeManager . debug ( `Error fetching user projects: ${ error } ` ) ;
748+ }
749+
750+ // Fetch organization projects
751+ try {
752+ let allOrgs : { login : string } [ ] = [ ] ;
753+ let orgsPage = 1 ;
754+ let hasMoreOrgs = true ;
755+
756+ while ( hasMoreOrgs ) {
757+ const { data : orgs } = await this . octokit . rest . orgs . listForAuthenticatedUser ( {
758+ per_page : 100 ,
759+ page : orgsPage ,
760+ } ) ;
761+
762+ allOrgs = [ ...allOrgs , ...orgs ] ;
763+ hasMoreOrgs = orgs . length === 100 ;
764+ orgsPage ++ ;
765+ }
766+
767+ for ( const org of allOrgs ) {
768+ try {
769+ let hasNextPage = true ;
770+ let cursor : string | null = null ;
771+
772+ while ( hasNextPage ) {
773+ const orgResponse : any = await this . octokit . graphql (
774+ GET_ORGANIZATION_PROJECTS ,
775+ {
776+ org : org . login ,
777+ first : 50 ,
778+ after : cursor ,
779+ } ,
780+ ) ;
781+
782+ if ( orgResponse ?. organization ?. projectsV2 ?. nodes ) {
783+ for ( const node of orgResponse . organization . projectsV2 . nodes ) {
784+ if ( ! seenIds . has ( node . id ) ) {
785+ seenIds . add ( node . id ) ;
786+ projects . push ( {
787+ id : node . id ,
788+ title : node . title ,
789+ number : node . number ,
790+ url : node . url ,
791+ closed : node . closed ,
792+ owner : org . login ,
793+ } ) ;
794+ }
795+ }
796+ }
797+
798+ hasNextPage = orgResponse ?. organization ?. projectsV2 ?. pageInfo ?. hasNextPage ?? false ;
799+ cursor = orgResponse ?. organization ?. projectsV2 ?. pageInfo ?. endCursor ?? null ;
800+ }
801+ } catch ( error ) {
802+ this . noticeManager . debug ( `Error fetching projects for org ${ org . login } : ${ error } ` ) ;
803+ }
804+ }
805+ } catch ( error ) {
806+ this . noticeManager . debug ( `Error fetching organizations: ${ error } ` ) ;
807+ }
808+
809+ this . noticeManager . debug ( `Found ${ projects . length } total projects` ) ;
810+ } catch ( error ) {
811+ this . noticeManager . error ( "Error fetching all projects" , error ) ;
812+ }
813+
814+ return projects ;
815+ }
816+
692817 /**
693818 * Fetch available projects for a repository (includes org projects)
694819 */
@@ -702,15 +827,12 @@ export class GitHubClient {
702827
703828 const projects : ProjectInfo [ ] = [ ] ;
704829
705- this . noticeManager . debug ( `[Projects] Fetching for owner='${ owner } ', repo='${ repo } '` ) ;
706-
707830 try {
708831 // First, try to get repository-linked projects
709832 let hasNextPage = true ;
710833 let cursor : string | null = null ;
711834
712835 while ( hasNextPage ) {
713- this . noticeManager . debug ( `[Projects] Querying repository projects: owner='${ owner } ', repo='${ repo } ', after='${ cursor } '` ) ;
714836 const response : any = await this . octokit . graphql (
715837 GET_REPOSITORY_PROJECTS ,
716838 {
@@ -735,12 +857,10 @@ export class GitHubClient {
735857
736858 hasNextPage = response ?. repository ?. projectsV2 ?. pageInfo ?. hasNextPage ?? false ;
737859 cursor = response ?. repository ?. projectsV2 ?. pageInfo ?. endCursor ?? null ;
738- this . noticeManager . debug ( `[Projects] Repo projects page: found=${ response ?. repository ?. projectsV2 ?. nodes ?. length ?? 0 } , hasNextPage=${ hasNextPage } ` ) ;
739860 }
740861
741862 // Also try to get organization projects if the owner is an org
742863 try {
743- this . noticeManager . debug ( `[Projects] Querying org projects: org='${ owner } ', after='${ cursor } '` ) ;
744864 hasNextPage = true ;
745865 cursor = null ;
746866
@@ -771,17 +891,13 @@ export class GitHubClient {
771891
772892 hasNextPage = orgResponse ?. organization ?. projectsV2 ?. pageInfo ?. hasNextPage ?? false ;
773893 cursor = orgResponse ?. organization ?. projectsV2 ?. pageInfo ?. endCursor ?? null ;
774- this . noticeManager . debug ( `[Projects] Org projects page: found=${ orgResponse ?. organization ?. projectsV2 ?. nodes ?. length ?? 0 } , hasNextPage=${ hasNextPage } ` ) ;
775894 }
776- } catch ( orgError ) {
777- // Owner is probably a user, not an org - that's fine
778- this . noticeManager . debug ( `Could not fetch org projects for ${ owner } : likely a user account` ) ;
779- this . noticeManager . debug ( `[Projects] Org projects error: ${ orgError } ` ) ;
895+ } catch {
896+ // Owner is probably a user, not an org - try user projects instead
780897 }
781898
782899 // Also try to get user projects if the owner is a user
783900 try {
784- this . noticeManager . debug ( `[Projects] Querying user projects: user='${ owner } ', after='${ cursor } '` ) ;
785901 hasNextPage = true ;
786902 cursor = null ;
787903
@@ -811,17 +927,14 @@ export class GitHubClient {
811927
812928 hasNextPage = userResponse ?. user ?. projectsV2 ?. pageInfo ?. hasNextPage ?? false ;
813929 cursor = userResponse ?. user ?. projectsV2 ?. pageInfo ?. endCursor ?? null ;
814- this . noticeManager . debug ( `[Projects] User projects page: found=${ userResponse ?. user ?. projectsV2 ?. nodes ?. length ?? 0 } , hasNextPage=${ hasNextPage } ` ) ;
815930 }
816- } catch ( userError ) {
817- this . noticeManager . debug ( `Could not fetch user projects for ${ owner } : likely an org account` ) ;
818- this . noticeManager . debug ( `[Projects] User projects error: ${ userError } ` ) ;
931+ } catch {
932+ // Owner is an org, not a user - that's fine
819933 }
820934
821935 this . noticeManager . debug (
822936 `Found ${ projects . length } projects for ${ owner } /${ repo } ` ,
823937 ) ;
824- this . noticeManager . debug ( `[Projects] Final project count for owner='${ owner } ', repo='${ repo } ': ${ projects . length } ` ) ;
825938 } catch ( error ) {
826939 this . noticeManager . debug (
827940 `Error fetching projects for ${ owner } /${ repo } : ${ error } ` ,
@@ -887,6 +1000,44 @@ export class GitHubClient {
8871000 }
8881001 }
8891002
1003+ /**
1004+ * Fetch status field options for a project (in GitHub's order)
1005+ */
1006+ public async fetchProjectStatusOptions ( projectId : string ) : Promise < ProjectStatusOption [ ] > {
1007+ if ( ! this . octokit ) {
1008+ return [ ] ;
1009+ }
1010+
1011+ try {
1012+ const response : any = await this . octokit . graphql ( GET_PROJECT_FIELDS , {
1013+ projectId,
1014+ } ) ;
1015+
1016+ if ( ! response ?. node ?. fields ?. nodes ) {
1017+ return [ ] ;
1018+ }
1019+
1020+ // Find the Status field (SingleSelectField with name "Status")
1021+ for ( const field of response . node . fields . nodes ) {
1022+ if ( field . name === 'Status' && field . options ) {
1023+ return field . options . map ( ( opt : any ) => ( {
1024+ id : opt . id ,
1025+ name : opt . name ,
1026+ color : opt . color ,
1027+ description : opt . description ,
1028+ } ) ) ;
1029+ }
1030+ }
1031+
1032+ return [ ] ;
1033+ } catch ( error ) {
1034+ this . noticeManager . debug (
1035+ `Error fetching status options for project ${ projectId } : ${ error } ` ,
1036+ ) ;
1037+ return [ ] ;
1038+ }
1039+ }
1040+
8901041 public dispose ( ) : void {
8911042 this . octokit = null ;
8921043 this . currentUser = "" ;
0 commit comments