From 2c3da59e275b69ebf984bb70954f42a7bcb0b49d Mon Sep 17 00:00:00 2001
From: Gwyneth Morgan <gwymor@tilde.club>
Date: Mon, 15 Jan 2024 07:07:22 -0800
Subject: [PATCH] Add ability to see open and closed issues at the same time
 (#28757)

By clicking the currently active "Open" or "Closed" filter button in the
issue list, the user can toggle that filter off in order to see all
issues regardless of state. The URL "state" parameter will be set to
"all" and the "Open"/"Closed" button will not show as active.
---
 models/issues/tracked_time.go       | 12 ++++++----
 models/issues/tracked_time_test.go  |  9 ++++++--
 routers/web/repo/issue.go           | 36 +++++++++++++++++++++--------
 templates/repo/issue/openclose.tmpl |  4 ++--
 4 files changed, 42 insertions(+), 19 deletions(-)

diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index 884a445d26..91c4832e49 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -340,7 +340,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
 }
 
 // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
-func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed bool) (int64, error) {
+func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) {
 	if len(opts.IssueIDs) <= MaxQueryParameters {
 		return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
 	}
@@ -363,7 +363,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed
 	return accum, nil
 }
 
-func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed bool, issueIDs []int64) (int64, error) {
+func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) {
 	sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
 		sess := db.GetEngine(ctx).
 			Table("tracked_time").
@@ -377,7 +377,9 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC
 		Time int64
 	}
 
-	return sumSession(opts, issueIDs).
-		And("issue.is_closed = ?", isClosed).
-		SumInt(new(trackedTime), "tracked_time.time")
+	session := sumSession(opts, issueIDs)
+	if !isClosed.IsNone() {
+		session = session.And("issue.is_closed = ?", isClosed.IsTrue())
+	}
+	return session.SumInt(new(trackedTime), "tracked_time.time")
 }
diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go
index 2774234e7b..9beb862ffb 100644
--- a/models/issues/tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -11,6 +11,7 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -119,11 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
 func TestGetIssueTotalTrackedTime(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, false)
+	ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 3682, ttt)
 
-	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, true)
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue)
 	assert.NoError(t, err)
 	assert.EqualValues(t, 0, ttt)
+
+	ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 3682, ttt)
 }
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 0d660e3b89..c8c9924a9e 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -237,10 +237,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 		}
 	}
 
-	isShowClosed := ctx.FormString("state") == "closed"
-	// if open issues are zero and close don't, use closed as default
+	var isShowClosed util.OptionalBool
+	switch ctx.FormString("state") {
+	case "closed":
+		isShowClosed = util.OptionalBoolTrue
+	case "all":
+		isShowClosed = util.OptionalBoolNone
+	default:
+		isShowClosed = util.OptionalBoolFalse
+	}
+	// if there are closed issues and no open issues, default to showing all issues
 	if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
-		isShowClosed = true
+		isShowClosed = util.OptionalBoolNone
 	}
 
 	if repo.IsTimetrackerEnabled(ctx) {
@@ -260,10 +268,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	}
 
 	var total int
-	if !isShowClosed {
-		total = int(issueStats.OpenCount)
-	} else {
+	switch isShowClosed {
+	case util.OptionalBoolTrue:
 		total = int(issueStats.ClosedCount)
+	case util.OptionalBoolNone:
+		total = int(issueStats.OpenCount + issueStats.ClosedCount)
+	default:
+		total = int(issueStats.OpenCount)
 	}
 	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
 
@@ -282,7 +293,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 			ReviewedID:        reviewedID,
 			MilestoneIDs:      mileIDs,
 			ProjectID:         projectID,
-			IsClosed:          util.OptionalBoolOf(isShowClosed),
+			IsClosed:          isShowClosed,
 			IsPull:            isPullOption,
 			LabelIDs:          labelIDs,
 			SortType:          sortType,
@@ -428,6 +439,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	ctx.Data["OpenCount"] = issueStats.OpenCount
 	ctx.Data["ClosedCount"] = issueStats.ClosedCount
 	linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
+	ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
+		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
+		mentionedID, projectID, assigneeID, posterID, archived)
 	ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
 		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
 		mentionedID, projectID, assigneeID, posterID, archived)
@@ -442,11 +456,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
 	ctx.Data["ProjectID"] = projectID
 	ctx.Data["AssigneeID"] = assigneeID
 	ctx.Data["PosterID"] = posterID
-	ctx.Data["IsShowClosed"] = isShowClosed
 	ctx.Data["Keyword"] = keyword
-	if isShowClosed {
+	switch isShowClosed {
+	case util.OptionalBoolTrue:
 		ctx.Data["State"] = "closed"
-	} else {
+	case util.OptionalBoolNone:
+		ctx.Data["State"] = "all"
+	default:
 		ctx.Data["State"] = "open"
 	}
 	ctx.Data["ShowArchivedLabels"] = archived
diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index ff5ec3c5a5..38848c51ac 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -1,5 +1,5 @@
 <div class="small-menu-items ui compact tiny menu">
-	<a class="{{if not .IsShowClosed}}active {{end}}item" href="{{.OpenLink}}">
+	<a class="{{if eq .State "open"}}active {{end}}item" href="{{if eq .State "open"}}{{.AllStatesLink}}{{else}}{{.OpenLink}}{{end}}">
 		{{if .PageIsMilestones}}
 			{{svg "octicon-milestone" 16 "gt-mr-3"}}
 		{{else if .PageIsPullList}}
@@ -9,7 +9,7 @@
 		{{end}}
 		{{ctx.Locale.PrettyNumber .OpenCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 	</a>
-	<a class="{{if .IsShowClosed}}active {{end}}item" href="{{.ClosedLink}}">
+	<a class="{{if eq .State "closed"}}active {{end}}item" href="{{if eq .State "closed"}}{{.AllStatesLink}}{{else}}{{.ClosedLink}}{{end}}">
 		{{svg "octicon-check" 16 "gt-mr-3"}}
 		{{ctx.Locale.PrettyNumber .ClosedCount}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 	</a>