enhance(project): filter project milestones by linked issues (#39958)

The project milestone filter listed every milestone of the repo, or of
all accessible org repos for org projects, including ones with no issue
in the project. That made the dropdown noisy and unhelpful.

Both repo and org projects now list only milestones that have at least
one issue linked to the project, via a new `ProjectID` option on
`FindMilestoneOptions`.

Fixes: https://github.com/go-gitea/gitea/issues/39150
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
Shivansh Gargandsilverwind authored and GitHub committed 2026-10-23 23:00:09 +00:00
1 parent efd69ab1ff
commit bcb4559c10
5 files changed
+28 -11

No files matched your search

+12 -6
View File
@@ -39,12 +39,13 @@ func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList)
// FindMilestoneOptions contain options to get milestones
type FindMilestoneOptions struct {
db.ListOptions
RepoID int64
IsClosed optional.Option[bool]
Name string
SortType string
RepoCond builder.Cond
RepoIDs []int64
RepoID int64
IsClosed optional.Option[bool]
Name string
SortType string
RepoCond builder.Cond
RepoIDs []int64
ProjectID int64
}
func (opts FindMilestoneOptions) ToConds() builder.Cond {
@@ -64,6 +65,11 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
if len(opts.Name) != 0 {
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
}
if opts.ProjectID != 0 {
cond = cond.And(builder.In("id", builder.Select("issue.milestone_id").From("issue").
InnerJoin("project_issue", "project_issue.issue_id = issue.id").
Where(builder.Eq{"project_issue.project_id": opts.ProjectID})))
}
return cond
}
+2 -1
View File
@@ -440,7 +440,8 @@ func ViewProject(ctx *context.Context) {
accessCond,
)
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoCond: repoCond,
RepoCond: repoCond,
ProjectID: project.ID,
})
if err != nil {
ctx.ServerError("GetOrgMilestones", err)
+4 -3
View File
@@ -380,10 +380,11 @@ func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*i
ctx.Data["ExclusiveLabelScopes"] = scopes
}
func renderMilestones(ctx *context.Context) {
func renderMilestones(ctx *context.Context, projectID int64) {
// Get milestones
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: ctx.Repo.Repository.ID,
RepoID: ctx.Repo.Repository.ID,
ProjectID: projectID,
})
if err != nil {
ctx.ServerError("GetAllRepoMilestones", err)
@@ -679,7 +680,7 @@ func Issues(ctx *context.Context) {
return
}
renderMilestones(ctx)
renderMilestones(ctx, 0)
if ctx.Written() {
return
}
+1 -1
View File
@@ -415,7 +415,7 @@ func ViewProject(ctx *context.Context) {
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
ctx.Data["AssigneeID"] = assigneeID
renderMilestones(ctx)
renderMilestones(ctx, project.ID)
if ctx.Written() {
return
}
+9
View File
@@ -240,6 +240,12 @@ func getProjectIssueIDs(t *testing.T, htmlDoc *HTMLDoc) map[int64]struct{} {
return ids
}
func getProjectMilestoneNames(htmlDoc *HTMLDoc) []string {
return htmlDoc.Find(".list-header-filters a:has(.octicon-milestone)").Map(func(_ int, s *goquery.Selection) string {
return strings.TrimSpace(s.Text())
})
}
func TestRepoProjectFilterByMilestone(t *testing.T) {
// Project 1 is on repo 1 (user2/repo1) and has issues:
// issue 1 (milestone_id=0), issue 2 (milestone_id=1), issue 3 (milestone_id=3), issue 5 (milestone_id=0)
@@ -257,6 +263,7 @@ func TestRepoProjectFilterByMilestone(t *testing.T) {
assert.Contains(t, issueIDs, int64(2))
assert.Contains(t, issueIDs, int64(3))
assert.Contains(t, issueIDs, int64(5))
assert.Equal(t, []string{"milestone1", "milestone3"}, getProjectMilestoneNames(htmlDoc))
})
t.Run("FilterByMilestone", func(t *testing.T) {
@@ -312,6 +319,7 @@ func TestOrgProjectFilterByMilestone(t *testing.T) {
Name: "org-test-milestone",
}
require.NoError(t, issues_model.NewMilestone(t.Context(), milestone))
require.NoError(t, issues_model.NewMilestone(t.Context(), &issues_model.Milestone{RepoID: repo.ID, Name: "unlinked-milestone"}))
issue16.MilestoneID = milestone.ID
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue16, "milestone_id"))
@@ -341,6 +349,7 @@ func TestOrgProjectFilterByMilestone(t *testing.T) {
issueIDs := getProjectIssueIDs(t, htmlDoc)
assert.Contains(t, issueIDs, issue16.ID)
assert.Contains(t, issueIDs, issue17.ID)
assert.Equal(t, []string{milestone.Name}, getProjectMilestoneNames(htmlDoc))
})
t.Run("FilterByMilestone", func(t *testing.T) {