From e6da7589fc63e39785a05426ed2ed6f970725166 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Tue, 8 Jul 2025 21:45:45 +0200 Subject: [PATCH] Display badge on blocked & blocking issues --- models/issues/issue_list.go | 68 ++++++++++++++++++++++++++++++++ options/locale/locale_en-US.ini | 4 ++ routers/web/repo/issue_list.go | 27 +++++++++++++ routers/web/user/home.go | 27 +++++++++++++ routers/web/user/notification.go | 26 ++++++++++++ templates/shared/issuelist.tmpl | 20 ++++++++++ web_src/css/repo/issue-label.css | 7 ++++ 7 files changed, 179 insertions(+) diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 26b93189b8..844f1d17bd 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -559,6 +559,74 @@ func (issues IssueList) LoadDiscussComments(ctx context.Context) error { return issues.loadComments(ctx, builder.Eq{"comment.type": CommentTypeComment}) } +// GetBlockedByCounts returns a map of issue ID to number of open issues that are blocking it +func (issues IssueList) GetBlockedByCount(ctx context.Context) (map[int64]int64, error) { + type BlockedByCount struct { + IssueID int64 + Count int64 + } + + bCounts := make([]*BlockedByCount, len(issues)) + ids := make([]int64, len(issues)) + for i, issue := range issues { + ids[i] = issue.ID + } + + sess := db.GetEngine(ctx).In("issue_id", ids) + err := sess.Select("issue_id, count(issue_dependency.id) as `count`"). + Join("INNER", "issue", "issue.id = issue_dependency.dependency_id"). + Where("is_closed = ?", false). + GroupBy("issue_id"). + OrderBy("issue_id"). + Table("issue_dependency"). + Find(&bCounts) + if err != nil { + return nil, err + } + + blockedByCountMap := make(map[int64]int64, len(issues)) + for _, c := range bCounts { + if c != nil { + blockedByCountMap[c.IssueID] = c.Count + } + } + + return blockedByCountMap, nil +} + +// GetBlockingCounts returns a map of issue ID to number of issues that are blocked by it +func (issues IssueList) GetBlockingCount(ctx context.Context) (map[int64]int64, error) { + type BlockingCount struct { + IssueID int64 + Count int64 + } + + bCounts := make([]*BlockingCount, 0, len(issues)) + ids := make([]int64, len(issues)) + for i, issue := range issues { + ids[i] = issue.ID + } + + sess := db.GetEngine(ctx).In("dependency_id", ids) + err := sess.Select("dependency_id as `issue_id`, count(id) as `count`"). + GroupBy("dependency_id"). + OrderBy("dependency_id"). + Table("issue_dependency"). + Find(&bCounts) + if err != nil { + return nil, err + } + + blockingCountMap := make(map[int64]int64, len(issues)) + for _, c := range bCounts { + if c != nil { + blockingCountMap[c.IssueID] = c.Count + } + } + + return blockingCountMap, nil +} + // GetApprovalCounts returns a map of issue ID to slice of approval counts // FIXME: only returns official counts due to double counting of non-official approvals func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9a2591e9ee..27b8ec0a67 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1793,6 +1793,10 @@ issues.dependency.add_error_dep_not_exist = Dependency does not exist. issues.dependency.add_error_dep_exists = Dependency already exists. issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two issues blocking each other. issues.dependency.add_error_dep_not_same_repo = Both issues must be in the same repository. +issues.dependency.blocking_count_1 = "This issue is blocking %d other issue." +issues.dependency.blocking_count_n = "This issue is blocking %d other issues." +issues.dependency.blocked_by_count_1 = "This issue is blocked by %d issue." +issues.dependency.blocked_by_count_n = "This issue is blocked by %d issues." issues.review.self.approval = You cannot approve your own pull request. issues.review.self.rejection = You cannot request changes on your own pull request. issues.review.approve = "approved these changes %s" diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index b55f4bcc90..209e3a7f06 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -654,6 +654,18 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 return } + blockingCounts, err := issues.GetBlockingCount(ctx) + if err != nil { + ctx.ServerError("BlockingCounts", err) + return + } + + blockedByCounts, err := issues.GetBlockedByCount(ctx) + if err != nil { + ctx.ServerError("BlockedByCounts", err) + return + } + if ctx.IsSigned { if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil { ctx.ServerError("LoadIsRead", err) @@ -718,6 +730,21 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6 return 0 } + ctx.Data["BlockingCounts"] = func(issueID int64) int64 { + counts, ok := blockingCounts[issueID] + if !ok { + return 0 + } + return counts + } + ctx.Data["BlockedByCounts"] = func(issueID int64) int64 { + counts, ok := blockedByCounts[issueID] + if !ok { + return 0 + } + return counts + } + retrieveProjectsForIssueList(ctx, repo) if ctx.Written() { return diff --git a/routers/web/user/home.go b/routers/web/user/home.go index b53a3daedb..b7a0cf079b 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -627,6 +627,33 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } return 0 } + + blockingCounts, err := issues.GetBlockingCount(ctx) + if err != nil { + ctx.ServerError("BlockingCounts", err) + return + } + + blockedByCounts, err := issues.GetBlockedByCount(ctx) + if err != nil { + ctx.ServerError("BlockedByCounts", err) + return + } + ctx.Data["BlockingCounts"] = func(issueID int64) int64 { + counts, ok := blockingCounts[issueID] + if !ok { + return 0 + } + return counts + } + ctx.Data["BlockedByCounts"] = func(issueID int64) int64 { + counts, ok := blockedByCounts[issueID] + if !ok { + return 0 + } + return counts + } + ctx.Data["CommitLastStatus"] = lastStatus ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["IssueStats"] = issueStats diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 610a9b8076..d0e2f8da13 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -311,6 +311,32 @@ func NotificationSubscriptions(ctx *context.Context) { return 0 } + blockingCounts, err := issues.GetBlockingCount(ctx) + if err != nil { + ctx.ServerError("BlockingCounts", err) + return + } + + blockedByCounts, err := issues.GetBlockedByCount(ctx) + if err != nil { + ctx.ServerError("BlockedByCounts", err) + return + } + ctx.Data["BlockingCounts"] = func(issueID int64) int64 { + counts, ok := blockingCounts[issueID] + if !ok { + return 0 + } + return counts + } + ctx.Data["BlockedByCounts"] = func(issueID int64) int64 { + counts, ok := blockedByCounts[issueID] + if !ok { + return 0 + } + return counts + } + ctx.Data["Status"] = 1 ctx.Data["Title"] = ctx.Tr("notification.subscriptions") diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 98c26b32dc..9adc6899fc 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -1,6 +1,10 @@
{{$approvalCounts := .ApprovalCounts}} + {{$blockedByCounts := .BlockedByCounts}} + {{$blockingCounts := .BlockingCounts}} {{range .Issues}} + {{$blockedByCount := call $blockedByCounts .ID}} + {{$blockingCount := call $blockingCounts .ID}}
@@ -22,6 +26,22 @@ {{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}} {{end}} {{end}} + {{if gt $blockedByCount 0}} +
+ + {{svg "octicon-blocked" 16}} + {{$blockedByCount}} + +
+ {{end}} + {{if and (gt $blockingCount 0) (not .IsClosed)}} +
+ + {{svg "octicon-report" 16}} + {{$blockingCount}} + +
+ {{end}} {{range .Labels}} {{ctx.RenderUtils.RenderLabel .}} diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css index f75c73b50f..fc4e8162db 100644 --- a/web_src/css/repo/issue-label.css +++ b/web_src/css/repo/issue-label.css @@ -56,3 +56,10 @@ top: 10px; right: 5px; } + +.label-blocking { + border: 1px solid var(--color-secondary) !important; + background: none transparent !important; + margin-left: 1px; + margin-right: 1px; +}