diff --git a/models/issues/label.go b/models/issues/label.go index 5b9898b3b27..0c3010601ad 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -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) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index ea70e1cc214..94958e414fc 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -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{ diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 3ef8826ce7b..0bc52993482 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -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...) diff --git a/routers/web/shared/issue/issue_label.go b/routers/web/shared/issue/issue_label.go index d8ebc987df1..aba0c2b1efe 100644 --- a/routers/web/shared/issue/issue_label.go +++ b/routers/web/shared/issue/issue_label.go @@ -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...) }