mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-19 15:48:30 +00:00
Backport #38893 by @HsukqiLee Closes #38872 Labels in the label selection dropdown (issue/PR sidebar, new issue form) were always listed alphabetically, so a scoped set like the default Priority labels showed up as Critical, High, Low, Medium even though each label carries an exclusive order. This adds `CompareLabelForDisplay`/`SortLabelsForDisplay` in `models/issues`: labels are grouped by their exclusive scope and sorted by exclusive order within a scope (unordered ones last), falling back to name order. The sorting is applied to the issue page sidebar data and the shared label filter data, so the filter dropdown on the issue list gets the same ordering. Unscoped labels are unaffected and still sort by name. Includes a unit test covering the default Priority label set. Co-authored-by: Hsukqi Lee <team@tsinbei.com>
This commit is contained in:
@@ -5,9 +5,11 @@
|
||||
package issues
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -191,6 +193,42 @@ func (l *Label) ExclusiveScope() string {
|
||||
return l.Name[:lastIndex]
|
||||
}
|
||||
|
||||
// CompareLabelForDisplay compares labels for displaying them in dropdowns or lists.
|
||||
// Labels are grouped by their exclusive scope, and labels within the same scope
|
||||
// are sorted by their exclusive order, where unordered labels (order 0) come last.
|
||||
// Labels without a scope are listed first and everything else falls back to name order.
|
||||
func CompareLabelForDisplay(a, b *Label) int {
|
||||
scopeA, scopeB := a.ExclusiveScope(), b.ExclusiveScope()
|
||||
if scopeA != scopeB {
|
||||
if scopeA == "" {
|
||||
return -1
|
||||
}
|
||||
if scopeB == "" {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(scopeA, scopeB)
|
||||
}
|
||||
if scopeA != "" {
|
||||
orderA, orderB := a.ExclusiveOrder, b.ExclusiveOrder
|
||||
if orderA <= 0 {
|
||||
orderA = math.MaxInt
|
||||
}
|
||||
if orderB <= 0 {
|
||||
orderB = math.MaxInt
|
||||
}
|
||||
if orderA != orderB {
|
||||
return cmp.Compare(orderA, orderB)
|
||||
}
|
||||
}
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
|
||||
// SortLabelsForDisplay sorts labels in place for displaying them in dropdowns or lists,
|
||||
// grouping them by their exclusive scope and respecting the exclusive order within each scope.
|
||||
func SortLabelsForDisplay(labels []*Label) {
|
||||
slices.SortStableFunc(labels, CompareLabelForDisplay)
|
||||
}
|
||||
|
||||
// NewLabel creates a new label
|
||||
func NewLabel(ctx context.Context, l *Label) error {
|
||||
color, err := label.NormalizeColor(l.Color)
|
||||
|
||||
@@ -54,6 +54,47 @@ func TestLabel_ExclusiveScope(t *testing.T) {
|
||||
assert.Equal(t, "scope/subscope", label.ExclusiveScope())
|
||||
}
|
||||
|
||||
func TestSortLabelsForDisplay(t *testing.T) {
|
||||
labels := []*issues_model.Label{
|
||||
{Name: "priority/low", Exclusive: true, ExclusiveOrder: 4},
|
||||
{Name: "priority/critical", Exclusive: true, ExclusiveOrder: 1},
|
||||
{Name: "priority/medium", Exclusive: true, ExclusiveOrder: 3},
|
||||
{Name: "priority/high", Exclusive: true, ExclusiveOrder: 2},
|
||||
{Name: "bug"},
|
||||
{Name: "enhancement"},
|
||||
{Name: "kind/question", Exclusive: true},
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
|
||||
names := make([]string, 0, len(labels))
|
||||
for _, l := range labels {
|
||||
names = append(names, l.Name)
|
||||
}
|
||||
assert.Equal(t, []string{
|
||||
"bug",
|
||||
"enhancement",
|
||||
"kind/question",
|
||||
"priority/critical",
|
||||
"priority/high",
|
||||
"priority/medium",
|
||||
"priority/low",
|
||||
}, names)
|
||||
|
||||
// labels without an exclusive order in the same scope are listed last, ordered by name
|
||||
labels = []*issues_model.Label{
|
||||
{Name: "scope/unordered-b", Exclusive: true},
|
||||
{Name: "scope/ordered", Exclusive: true, ExclusiveOrder: 1},
|
||||
{Name: "scope/unordered-a", Exclusive: true},
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
|
||||
names = names[:0]
|
||||
for _, l := range labels {
|
||||
names = append(names, l.Name)
|
||||
}
|
||||
assert.Equal(t, []string{"scope/ordered", "scope/unordered-a", "scope/unordered-b"}, names)
|
||||
}
|
||||
|
||||
func TestNewLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels := []*issues_model.Label{
|
||||
|
||||
@@ -516,6 +516,7 @@ func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(labels)
|
||||
labelsData.RepoLabels = labels
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
@@ -523,6 +524,7 @@ func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(orgLabels)
|
||||
labelsData.OrgLabels = orgLabels
|
||||
}
|
||||
labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
|
||||
|
||||
@@ -38,6 +38,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return ret
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(repoLabels)
|
||||
allLabels = append(allLabels, repoLabels...)
|
||||
}
|
||||
|
||||
@@ -47,6 +48,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return ret
|
||||
}
|
||||
issues_model.SortLabelsForDisplay(orgLabels)
|
||||
allLabels = append(allLabels, orgLabels...)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user