From 0ef988d4d3fbf9afa19f85a9dc3aa6d5a62c713b Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 02 Oct 2025 17:32:16 +0000 Subject: [PATCH] appview/{models,state}: implement good first issues handler Signed-off-by: Anirudh Oppiliappan --- appview/models/repo.go | 5 +++++ appview/pages/pages.go | 12 ++++++++++++ appview/state/gfi.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ appview/state/router.go | 2 ++ 4 file(s) changed, 157 insertion(s)(+), 0 deletion(s)(-) diff --git a/appview/models/repo.go b/appview/models/repo.go --- a/appview/models/repo.go +++ b/appview/models/repo.go @@ -86,3 +86,8 @@ RepoAt syntax.ATURI LabelAt syntax.ATURI } + +type RepoGroup struct { + Repo *Repo + Issues []Issue +} diff --git a/appview/pages/pages.go b/appview/pages/pages.go --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -312,6 +312,18 @@ return p.execute("timeline/timeline", w, params) } +type GoodFirstIssuesParams struct { + LoggedInUser *oauth.User + Issues []models.Issue + RepoGroups []*models.RepoGroup + LabelDefs map[string]*models.LabelDefinition + Page pagination.Page +} + +func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { + return p.execute("goodfirstissues/index", w, params) +} + type UserProfileSettingsParams struct { LoggedInUser *oauth.User Tabs []map[string]any diff --git a/appview/state/gfi.go b/appview/state/gfi.go new file mode 100644 --- /dev/null +++ b/appview/state/gfi.go @@ -0,0 +1,138 @@ +package state + +import ( + "fmt" + "log" + "net/http" + "sort" + + "tangled.org/core/api/tangled" + "tangled.org/core/appview/db" + "tangled.org/core/appview/models" + "tangled.org/core/appview/pages" + "tangled.org/core/appview/pagination" + "tangled.org/core/consts" +) + +func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetUser(r) + + page, ok := r.Context().Value("page").(pagination.Page) + if !ok { + page = pagination.FirstPage() + } + + goodFirstIssueLabel := fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue") + + repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel)) + if err != nil { + log.Println("failed to get repo labels", err) + s.pages.Error503(w) + return + } + + if len(repoLabels) == 0 { + s.pages.GoodFirstIssues(w, pages.GoodFirstIssuesParams{ + LoggedInUser: user, + RepoGroups: []*models.RepoGroup{}, + LabelDefs: make(map[string]*models.LabelDefinition), + Page: page, + }) + return + } + + repoUris := make([]string, 0, len(repoLabels)) + for _, rl := range repoLabels { + repoUris = append(repoUris, rl.RepoAt.String()) + } + + allIssues, err := db.GetIssues( + s.db, + db.FilterIn("repo_at", repoUris), + db.FilterEq("open", 1), + ) + if err != nil { + log.Println("failed to get issues", err) + s.pages.Error503(w) + return + } + + var goodFirstIssues []models.Issue + for _, issue := range allIssues { + if issue.Labels.ContainsLabel(goodFirstIssueLabel) { + goodFirstIssues = append(goodFirstIssues, issue) + } + } + + repoGroups := make(map[string]*models.RepoGroup) + for _, issue := range goodFirstIssues { + repoKey := fmt.Sprintf("%s/%s", issue.Repo.Did, issue.Repo.Name) + if group, exists := repoGroups[repoKey]; exists { + group.Issues = append(group.Issues, issue) + } else { + repoGroups[repoKey] = &models.RepoGroup{ + Repo: issue.Repo, + Issues: []models.Issue{issue}, + } + } + } + + var sortedGroups []*models.RepoGroup + for _, group := range repoGroups { + sortedGroups = append(sortedGroups, group) + } + + sort.Slice(sortedGroups, func(i, j int) bool { + return sortedGroups[i].Repo.Name < sortedGroups[j].Repo.Name + }) + + groupStart := page.Offset + groupEnd := page.Offset + page.Limit + if groupStart > len(sortedGroups) { + groupStart = len(sortedGroups) + } + if groupEnd > len(sortedGroups) { + groupEnd = len(sortedGroups) + } + + paginatedGroups := sortedGroups[groupStart:groupEnd] + + var allIssuesFromGroups []models.Issue + for _, group := range paginatedGroups { + allIssuesFromGroups = append(allIssuesFromGroups, group.Issues...) + } + + var allLabelDefs []models.LabelDefinition + if len(allIssuesFromGroups) > 0 { + labelDefUris := make(map[string]bool) + for _, issue := range allIssuesFromGroups { + for labelDefUri := range issue.Labels.Inner() { + labelDefUris[labelDefUri] = true + } + } + + uriList := make([]string, 0, len(labelDefUris)) + for uri := range labelDefUris { + uriList = append(uriList, uri) + } + + if len(uriList) > 0 { + allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList)) + if err != nil { + log.Println("failed to fetch labels", err) + } + } + } + + labelDefsMap := make(map[string]*models.LabelDefinition) + for i := range allLabelDefs { + labelDefsMap[allLabelDefs[i].AtUri().String()] = &allLabelDefs[i] + } + + s.pages.GoodFirstIssues(w, pages.GoodFirstIssuesParams{ + LoggedInUser: user, + RepoGroups: paginatedGroups, + LabelDefs: labelDefsMap, + Page: page, + }) +} diff --git a/appview/state/router.go b/appview/state/router.go --- a/appview/state/router.go +++ b/appview/state/router.go @@ -132,6 +132,8 @@ // r.Post("/import", s.ImportRepo) }) + r.Get("/goodfirstissues", s.GoodFirstIssues) + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { r.Post("/", s.Follow) r.Delete("/", s.Follow) -- tangled.sh