From 3abbf5fc21e2ea61d761b90d8e14f964ee5596a7 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 30 Apr 2023 13:01:12 -0400
Subject: [PATCH] Improve milestone filter on issues page (#22423) (#24440)

Backport #22423 by @lunny

Now we have `All milestones`, `No milestones`, `Open milestones` and
`Closed milestones`.
Fix #11924
Fix #22411

<img width="1166" alt="image"
src="https://user-images.githubusercontent.com/81045/212243375-95eea035-a972-44b8-8088-53db614cb07e.png">

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/db/search.go                           |  2 +-
 models/issues/issue.go                        |  8 +++-
 options/locale/locale_en-US.ini               |  5 ++-
 routers/web/repo/issue.go                     | 35 ++++++++++++-----
 templates/repo/issue/list.tmpl                | 27 +++++++++++--
 .../repo/issue/milestone/select_menu.tmpl     | 39 +++++++++++++++++++
 templates/repo/issue/new_form.tmpl            | 39 +------------------
 .../repo/issue/view_content/sidebar.tmpl      | 39 +------------------
 8 files changed, 101 insertions(+), 93 deletions(-)
 create mode 100644 templates/repo/issue/milestone/select_menu.tmpl

diff --git a/models/db/search.go b/models/db/search.go
index 26e082756a..105cb64c41 100644
--- a/models/db/search.go
+++ b/models/db/search.go
@@ -31,5 +31,5 @@ const (
 const (
 	// Which means a condition to filter the records which don't match any id.
 	// It's different from zero which means the condition could be ignored.
-	NoneID = -1
+	NoConditionID = -1
 )
diff --git a/models/issues/issue.go b/models/issues/issue.go
index c8d148dd86..2dae8848c2 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -1266,7 +1266,9 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) {
 		applySubscribedCondition(sess, opts.SubscriberID)
 	}
 
-	if len(opts.MilestoneIDs) > 0 {
+	if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID {
+		sess.And("issue.milestone_id = 0")
+	} else if len(opts.MilestoneIDs) > 0 {
 		sess.In("issue.milestone_id", opts.MilestoneIDs)
 	}
 
@@ -1280,7 +1282,7 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) {
 	if opts.ProjectID > 0 {
 		sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
 			And("project_issue.project_id=?", opts.ProjectID)
-	} else if opts.ProjectID == db.NoneID { // show those that are in no project
+	} else if opts.ProjectID == db.NoConditionID { // show those that are in no project
 		sess.And(builder.NotIn("issue.id", builder.Select("issue_id").From("project_issue")))
 	}
 
@@ -1680,6 +1682,8 @@ func getIssueStatsChunk(opts *IssueStatsOptions, issueIDs []int64) (*IssueStats,
 
 		if opts.MilestoneID > 0 {
 			sess.And("issue.milestone_id = ?", opts.MilestoneID)
+		} else if opts.MilestoneID == db.NoConditionID {
+			sess.And("issue.milestone_id = 0")
 		}
 
 		if opts.ProjectID > 0 {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 5154aadb68..8169cba108 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1310,7 +1310,10 @@ issues.filter_label = Label
 issues.filter_label_exclude = `Use <code>alt</code> + <code>click/enter</code> to exclude labels`
 issues.filter_label_no_select = All labels
 issues.filter_milestone = Milestone
-issues.filter_milestone_no_select = All milestones
+issues.filter_milestone_all = All milestones
+issues.filter_milestone_none = No milestones
+issues.filter_milestone_open = Open milestones
+issues.filter_milestone_closed = Closed milestones
 issues.filter_project = Project
 issues.filter_project_all = All projects
 issues.filter_project_none = No project
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 3868e895f0..4f370c610d 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -237,7 +237,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
 
 	var mileIDs []int64
-	if milestoneID > 0 {
+	if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
 		mileIDs = []int64{milestoneID}
 	}
 
@@ -438,14 +438,8 @@ func Issues(ctx *context.Context) {
 		return
 	}
 
-	var err error
-	// Get milestones
-	ctx.Data["Milestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
-		RepoID: ctx.Repo.Repository.ID,
-		State:  api.StateType(ctx.FormString("state")),
-	})
-	if err != nil {
-		ctx.ServerError("GetAllRepoMilestones", err)
+	renderMilestones(ctx)
+	if ctx.Written() {
 		return
 	}
 
@@ -454,6 +448,29 @@ func Issues(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplIssues)
 }
 
+func renderMilestones(ctx *context.Context) {
+	// Get milestones
+	milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
+		RepoID: ctx.Repo.Repository.ID,
+		State:  api.StateAll,
+	})
+	if err != nil {
+		ctx.ServerError("GetAllRepoMilestones", err)
+		return
+	}
+
+	openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
+	for _, milestone := range milestones {
+		if milestone.IsClosed {
+			closedMilestones = append(closedMilestones, milestone)
+		} else {
+			openMilestones = append(openMilestones, milestone)
+		}
+	}
+	ctx.Data["OpenMilestones"] = openMilestones
+	ctx.Data["ClosedMilestones"] = closedMilestones
+}
+
 // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
 func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
 	var err error
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index ddc197a889..a594801093 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -63,7 +63,7 @@
 					</div>
 
 					<!-- Milestone -->
-					<div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item">
+					<div class="ui {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}} dropdown jump item">
 						<span class="text">
 							{{.locale.Tr "repo.issues.filter_milestone"}}
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -73,9 +73,28 @@
 								<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
 								<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_milestone"}}">
 							</div>
-							<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">{{.locale.Tr "repo.issues.filter_milestone_no_select"}}</a>
-							{{range .Milestones}}
-								<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected{{end}}{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">{{.Name}}</a>
+							<div class="divider"></div>
+							<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">{{.locale.Tr "repo.issues.filter_milestone_all"}}</a>
+							<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">{{.locale.Tr "repo.issues.filter_milestone_none"}}</a>
+							{{if .OpenMilestones}}
+								<div class="divider"></div>
+								<div class="header">{{.locale.Tr "repo.issues.filter_milestone_open"}}</div>
+								{{range .OpenMilestones}}
+								<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">
+									{{svg "octicon-milestone" 16 "mr-2"}}
+									{{.Name}}
+								</a>
+								{{end}}
+							{{end}}
+							{{if .ClosedMilestones}}
+								<div class="divider"></div>
+								<div class="header">{{.locale.Tr "repo.issues.filter_milestone_closed"}}</div>
+								{{range .ClosedMilestones}}
+								<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}">
+									{{svg "octicon-milestone" 16 "mr-2"}}
+									{{.Name}}
+								</a>
+								{{end}}
 							{{end}}
 						</div>
 					</div>
diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl
new file mode 100644
index 0000000000..6f67b5d523
--- /dev/null
+++ b/templates/repo/issue/milestone/select_menu.tmpl
@@ -0,0 +1,39 @@
+<div class="header" style="text-transform: none;font-size:16px;">{{.locale.Tr "repo.issues.new.add_milestone_title"}}</div>
+{{if or .OpenMilestones .ClosedMilestones}}
+	<div class="ui icon search input">
+		<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
+		<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_milestones"}}">
+	</div>
+	<div class="divider"></div>
+{{end}}
+<div class="no-select item">{{.locale.Tr "repo.issues.new.clear_milestone"}}</div>
+{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
+	<div class="header" style="text-transform: none;font-size:14px;">
+		{{.locale.Tr "repo.issues.new.no_items"}}
+	</div>
+{{else}}
+	{{if .OpenMilestones}}
+		<div class="divider"></div>
+		<div class="header">
+			{{.locale.Tr "repo.issues.new.open_milestone"}}
+		</div>
+		{{range .OpenMilestones}}
+			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
+				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{.Name}}
+			</a>
+		{{end}}
+	{{end}}
+	{{if .ClosedMilestones}}
+		<div class="divider"></div>
+		<div class="header">
+			{{.locale.Tr "repo.issues.new.closed_milestone"}}
+		</div>
+		{{range .ClosedMilestones}}
+			<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
+				{{svg "octicon-milestone" 16 "gt-mr-2"}}
+				{{.Name}}
+			</a>
+		{{end}}
+	{{end}}
+{{end}}
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index c21d0c1689..25d09cf2bc 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -93,44 +93,7 @@
 					{{end}}
 				</span>
 				<div class="menu">
-					<div class="header" style="text-transform: none;font-size:16px;">{{.locale.Tr "repo.issues.new.add_milestone_title"}}</div>
-					{{if or .OpenMilestones .ClosedMilestones}}
-					<div class="ui icon search input">
-						<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
-						<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_milestones"}}">
-					</div>
-					{{end}}
-					<div class="no-select item">{{.locale.Tr "repo.issues.new.clear_milestone"}}</div>
-					{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
-						<div class="header" style="text-transform: none;font-size:14px;">
-							{{.locale.Tr "repo.issues.new.no_items"}}
-						</div>
-					{{else}}
-						{{if .OpenMilestones}}
-							<div class="divider"></div>
-							<div class="header">
-								{{.locale.Tr "repo.issues.new.open_milestone"}}
-							</div>
-							{{range .OpenMilestones}}
-								<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-									{{svg "octicon-milestone" 16 "gt-mr-2"}}
-									{{.Name}}
-								</a>
-							{{end}}
-						{{end}}
-						{{if .ClosedMilestones}}
-							<div class="divider"></div>
-							<div class="header">
-								{{.locale.Tr "repo.issues.new.closed_milestone"}}
-							</div>
-							{{range .ClosedMilestones}}
-								<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-									{{svg "octicon-milestone" 16 "gt-mr-2"}}
-									{{.Name}}
-								</a>
-							{{end}}
-						{{end}}
-					{{end}}
+					{{template "repo/issue/milestone/select_menu" .}}
 				</div>
 			</div>
 			<div class="ui select-milestone list">
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 0d796af0f3..8b788d646d 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -161,44 +161,7 @@
 				{{end}}
 			</a>
 			<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
-				<div class="header" style="text-transform: none;font-size:16px;">{{.locale.Tr "repo.issues.new.add_milestone_title"}}</div>
-				{{if or .OpenMilestones .ClosedMilestones}}
-				<div class="ui icon search input">
-					<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i>
-					<input type="text" placeholder="{{.locale.Tr "repo.issues.filter_milestones"}}">
-				</div>
-				{{end}}
-				<div class="no-select item">{{.locale.Tr "repo.issues.new.clear_milestone"}}</div>
-				{{if and (not .OpenMilestones) (not .ClosedMilestones)}}
-					<div class="header" style="text-transform: none;font-size:14px;">
-						{{.locale.Tr "repo.issues.new.no_items"}}
-					</div>
-				{{else}}
-					{{if .OpenMilestones}}
-						<div class="divider"></div>
-						<div class="header">
-							{{.locale.Tr "repo.issues.new.open_milestone"}}
-						</div>
-						{{range .OpenMilestones}}
-							<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-								{{svg "octicon-milestone" 16 "gt-mr-2"}}
-								{{.Name}}
-							</a>
-						{{end}}
-					{{end}}
-					{{if .ClosedMilestones}}
-						<div class="divider"></div>
-						<div class="header">
-							{{.locale.Tr "repo.issues.new.closed_milestone"}}
-						</div>
-						{{range .ClosedMilestones}}
-							<a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}">
-								{{svg "octicon-milestone" 16 "gt-mr-2"}}
-								{{.Name}}
-							</a>
-						{{end}}
-					{{end}}
-				{{end}}
+				{{template "repo/issue/milestone/select_menu" .}}
 			</div>
 		</div>
 		<div class="ui select-milestone list">