feat(api): add sort parameter to list issues API (#7211)

- Add the `sort` parameter to the `/api/v1/{repo}/{owner}/issues` API endpoint. Default behavior is preserved.
- Resolves forgejo/forgejo#4173
- Add (non-exhaustive) integration testing.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7211
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
Gusted 2025-03-17 09:02:57 +00:00 committed by Earl Warren
parent a624b6a8f4
commit 7d6d4f94ee
4 changed files with 97 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"os" "os"
"runtime/pprof" "runtime/pprof"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -280,6 +281,33 @@ const (
SortByDeadlineAsc = internal.SortByDeadlineAsc SortByDeadlineAsc = internal.SortByDeadlineAsc
) )
// ParseSortBy parses the `sortBy` string and returns the associated `SortBy`
// value, if one exists. Otherwise return `defaultSortBy`.
func ParseSortBy(sortBy string, defaultSortBy internal.SortBy) internal.SortBy {
switch strings.ToLower(sortBy) {
case "relevance":
return SortByScore
case "latest":
return SortByCreatedDesc
case "oldest":
return SortByCreatedAsc
case "recentupdate":
return SortByUpdatedDesc
case "leastupdate":
return SortByUpdatedAsc
case "mostcomment":
return SortByCommentsDesc
case "leastcomment":
return SortByCommentsAsc
case "nearduedate":
return SortByDeadlineAsc
case "farduedate":
return SortByDeadlineDesc
default:
return defaultSortBy
}
}
// SearchIssues search issues by options. // SearchIssues search issues by options.
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) { func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
indexer := *globalIndexer.Load() indexer := *globalIndexer.Load()

View file

@ -397,6 +397,12 @@ func ListIssues(ctx *context.APIContext) {
// in: query // in: query
// description: page size of results // description: page size of results
// type: integer // type: integer
// - name: sort
// in: query
// description: Type of sort
// type: string
// enum: [relevance, latest, oldest, recentupdate, leastupdate, mostcomment, leastcomment, nearduedate, farduedate]
// default: latest
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/IssueList" // "$ref": "#/responses/IssueList"
@ -510,7 +516,7 @@ func ListIssues(ctx *context.APIContext) {
RepoIDs: []int64{ctx.Repo.Repository.ID}, RepoIDs: []int64{ctx.Repo.Repository.ID},
IsPull: isPull, IsPull: isPull,
IsClosed: isClosed, IsClosed: isClosed,
SortBy: issue_indexer.SortByCreatedDesc, SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc),
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = optional.Some(since) searchOpt.UpdatedAfterUnix = optional.Some(since)

View file

@ -8628,6 +8628,24 @@
"description": "page size of results", "description": "page size of results",
"name": "limit", "name": "limit",
"in": "query" "in": "query"
},
{
"enum": [
"relevance",
"latest",
"oldest",
"recentupdate",
"leastupdate",
"mostcomment",
"leastcomment",
"nearduedate",
"farduedate"
],
"type": "string",
"default": "latest",
"description": "Type of sort",
"name": "sort",
"in": "query"
} }
], ],
"responses": { "responses": {

View file

@ -74,6 +74,50 @@ func TestAPIListIssues(t *testing.T) {
if assert.Len(t, apiIssues, 1) { if assert.Len(t, apiIssues, 1) {
assert.EqualValues(t, 1, apiIssues[0].ID) assert.EqualValues(t, 1, apiIssues[0].ID)
} }
t.Run("Sort", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
link.RawQuery = url.Values{"token": {token}, "sort": {"oldest"}}.Encode()
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
if assert.Len(t, apiIssues, 4) {
assert.EqualValues(t, 1, apiIssues[0].ID)
assert.EqualValues(t, 2, apiIssues[1].ID)
assert.EqualValues(t, 3, apiIssues[2].ID)
assert.EqualValues(t, 11, apiIssues[3].ID)
}
link.RawQuery = url.Values{"token": {token}, "sort": {"newest"}}.Encode()
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
if assert.Len(t, apiIssues, 4) {
assert.EqualValues(t, 11, apiIssues[0].ID)
assert.EqualValues(t, 3, apiIssues[1].ID)
assert.EqualValues(t, 2, apiIssues[2].ID)
assert.EqualValues(t, 1, apiIssues[3].ID)
}
link.RawQuery = url.Values{"token": {token}, "sort": {"recentupdate"}}.Encode()
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
if assert.Len(t, apiIssues, 4) {
assert.EqualValues(t, 11, apiIssues[0].ID)
assert.EqualValues(t, 1, apiIssues[1].ID)
assert.EqualValues(t, 2, apiIssues[2].ID)
assert.EqualValues(t, 3, apiIssues[3].ID)
}
link.RawQuery = url.Values{"token": {token}, "sort": {"leastupdate"}}.Encode()
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
if assert.Len(t, apiIssues, 4) {
assert.EqualValues(t, 3, apiIssues[0].ID)
assert.EqualValues(t, 2, apiIssues[1].ID)
assert.EqualValues(t, 1, apiIssues[2].ID)
assert.EqualValues(t, 11, apiIssues[3].ID)
}
})
} }
func TestAPIListIssuesPublicOnly(t *testing.T) { func TestAPIListIssuesPublicOnly(t *testing.T) {