Skip to content

feat: Add sorting by exclusive labels (issue priority) #33206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 68 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
bbf913a
Add sorting of exlusive labels.
telackey Jan 10, 2025
104e4c5
Adjust format
telackey Jan 10, 2025
182aed4
yaml support
telackey Jan 10, 2025
2fa535d
fmt
telackey Jan 10, 2025
041b352
fmt
telackey Jan 10, 2025
4e89634
sirt
telackey Jan 10, 2025
7b4563c
refactor
telackey Jan 10, 2025
6feba52
Merge branch 'go-gitea:main' into telackey/sort
telackey Jan 14, 2025
de58736
Tabs not spaces
telackey Feb 17, 2025
d1586e5
Lint
telackey Feb 17, 2025
591acd4
Lint
telackey Feb 17, 2025
fe195e2
Merge branch 'main' into telackey/sort
telackey Feb 17, 2025
f3d2f67
Migration
telackey Feb 17, 2025
f4d8bfb
Merge branch 'main' into telackey/sort
telackey Feb 17, 2025
b1c413f
Merge branch 'main' into telackey/sort
telackey Feb 18, 2025
71e4c27
Merge branch 'main' into telackey/sort
telackey Feb 18, 2025
f54af1e
Merge branch 'main' into telackey/sort
telackey Feb 19, 2025
cee7c08
Merge branch 'main' into telackey/sort
telackey Feb 21, 2025
b22ae56
Merge branch 'main' into telackey/sort
telackey Feb 28, 2025
b384bb1
fix ui
wxiaoguang Feb 28, 2025
89b56b8
fine tune
wxiaoguang Feb 28, 2025
209d0f9
fine tune style
wxiaoguang Feb 28, 2025
dc495e4
Bump migration up
telackey Mar 5, 2025
13fd9cd
Merge branch 'main' into telackey/sort
telackey Mar 5, 2025
817fc88
Lint
telackey Mar 5, 2025
8da23e7
Make sure to pull org labels too.
telackey Mar 6, 2025
6bd0e2d
Merge branch 'main' into telackey/sort
telackey Mar 6, 2025
d12ea5f
Add const for 'scope-' prefix.
telackey Mar 6, 2025
7df700e
Merge branch 'telackey/sort' of ssh://github.com/telackey/gitea into …
telackey Mar 6, 2025
83078ed
Add const for 'scope-' prefix.
telackey Mar 6, 2025
6e03264
Show sort order on the rendered label.
telackey Mar 6, 2025
c2c057d
Switch to DB indexer if the sort is based on label scope.
telackey Mar 6, 2025
385d454
Merge branch 'main' into telackey/sort
telackey Mar 7, 2025
5e74d78
Merge branch 'main' into telackey/sort
telackey Mar 10, 2025
1216ff9
Merge branch 'main' into telackey/sort
telackey Mar 11, 2025
b615d1b
Merge branch 'main' into telackey/sort
telackey Mar 12, 2025
8bf059f
Rename file
telackey Mar 25, 2025
012cb9e
Merge branch 'main' into telackey/sort
telackey Mar 25, 2025
31eff1f
Merge branch 'main' into telackey/sort
telackey Mar 25, 2025
ec91d65
lint
telackey Mar 25, 2025
6bbda85
Merge branch 'main' into telackey/sort
telackey Mar 26, 2025
399be95
Merge branch 'main' into telackey/sort
telackey Mar 26, 2025
fdb2dd0
Re-use results of a keyword search as list of matching IDs for an 'IN…
telackey Apr 3, 2025
f8d395c
Merge branch 'telackey/sort' of github.com:telackey/gitea into telack…
telackey Apr 3, 2025
d0faa52
Typo in comment
telackey Apr 3, 2025
25c0718
Use COALESCE to make sure nulls are last regardless of DB.
telackey Apr 4, 2025
e90725e
Use COALESCE to make sure nulls are last regardless of DB.
telackey Apr 4, 2025
2937c6d
Don't support options.IssuesIDs outside the DB indexer for now
telackey Apr 4, 2025
271e65b
Missed processing ExclusiveOrder when creating a new label from the o…
telackey Apr 4, 2025
c5bb344
Rename migration
telackey Apr 4, 2025
a2e355e
Rename migration
telackey Apr 4, 2025
c666ad8
Merge branch 'main' into telackey/sort
telackey Apr 4, 2025
28895b2
lint
telackey Apr 4, 2025
36ad3d6
lint
telackey Apr 4, 2025
3490d1c
lint
telackey Apr 4, 2025
864ba8b
lint
telackey Apr 4, 2025
54f6d71
Merge branch 'main' into telackey/sort
telackey Apr 4, 2025
d9ea3e4
Merge branch 'main' into telackey/sort
telackey Apr 7, 2025
898828a
Treat 0 as a null value for exclusive order.
telackey Apr 7, 2025
e95c60b
Treat 0 as a null value for exclusive order.
telackey Apr 7, 2025
1b81927
lint
telackey Apr 7, 2025
a713b4c
Merge branch 'main' into telackey/sort
wxiaoguang Apr 9, 2025
259f134
fix
wxiaoguang Apr 9, 2025
ef94ae9
fix typo
wxiaoguang Apr 9, 2025
3c71376
fix render, fix access token scope
wxiaoguang Apr 9, 2025
61e2015
avoid duplicate label queries
wxiaoguang Apr 9, 2025
f3b9dd4
fix search issue with ids
wxiaoguang Apr 9, 2025
13f2203
Merge branch 'main' into telackey/sort
wxiaoguang Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions models/issues/issue_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"xorm.io/xorm"
)

const ScopeSortPrefix = "scope-"

// IssuesOptions represents options of an issue.
type IssuesOptions struct { //nolint
Paginator *db.ListOptions
Expand Down Expand Up @@ -70,6 +72,17 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption
// applySorts sort an issues-related session based on the provided
// sortType string
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
// Since this sortType is dynamically created, it has to be treated specially.
if strings.HasPrefix(sortType, ScopeSortPrefix) {
scope := strings.TrimPrefix(sortType, ScopeSortPrefix)
sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
// "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null
sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%")
// Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int)
sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id")
return
}

switch sortType {
case "oldest":
sess.Asc("issue.created_unix").Asc("issue.id")
Expand Down
3 changes: 2 additions & 1 deletion models/issues/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type Label struct {
OrgID int64 `xorm:"INDEX"`
Name string
Exclusive bool
ExclusiveOrder int `xorm:"DEFAULT 0"` // 0 means no exclusive order
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
Expand Down Expand Up @@ -236,7 +237,7 @@ func UpdateLabel(ctx context.Context, l *Label) error {
}
l.Color = color

return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
}

// DeleteLabel delete a label
Expand Down
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func prepareMigrationTasks() []*migration {
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
}
return preparedMigrations
}
Expand Down
16 changes: 16 additions & 0 deletions models/migrations/v1_24/v319.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_24 //nolint

import (
"xorm.io/xorm"
)

func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error {
type Label struct {
ExclusiveOrder int `xorm:"DEFAULT 0"`
}

return x.Sync(new(Label))
}
17 changes: 10 additions & 7 deletions modules/indexer/issues/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package db
import (
"context"
"strings"
"sync"

"code.gitea.io/gitea/models/db"
issue_model "code.gitea.io/gitea/models/issues"
Expand All @@ -18,7 +19,7 @@ import (
"xorm.io/builder"
)

var _ internal.Indexer = &Indexer{}
var _ internal.Indexer = (*Indexer)(nil)

// Indexer implements Indexer interface to use database's like search
type Indexer struct {
Expand All @@ -29,11 +30,9 @@ func (i *Indexer) SupportedSearchModes() []indexer.SearchMode {
return indexer.SearchModesExactWords()
}

func NewIndexer() *Indexer {
return &Indexer{
Indexer: &inner_db.Indexer{},
}
}
var GetIndexer = sync.OnceValue(func() *Indexer {
return &Indexer{Indexer: &inner_db.Indexer{}}
})

// Index dummy function
func (i *Indexer) Index(_ context.Context, _ ...*internal.IndexerData) error {
Expand Down Expand Up @@ -122,7 +121,11 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
}, nil
}

ids, total, err := issue_model.IssueIDs(ctx, opt, cond)
return i.FindWithIssueOptions(ctx, opt, cond)
}

func (i *Indexer) FindWithIssueOptions(ctx context.Context, opt *issue_model.IssuesOptions, otherConds ...builder.Cond) (*internal.SearchResult, error) {
ids, total, err := issue_model.IssueIDs(ctx, opt, otherConds...)
if err != nil {
return nil, err
}
Expand Down
8 changes: 6 additions & 2 deletions modules/indexer/issues/db/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package db
import (
"context"
"fmt"
"strings"

"code.gitea.io/gitea/models/db"
issue_model "code.gitea.io/gitea/models/issues"
Expand Down Expand Up @@ -34,7 +35,11 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
case internal.SortByDeadlineAsc:
sortType = "nearduedate"
default:
sortType = "newest"
if strings.HasPrefix(string(options.SortBy), issue_model.ScopeSortPrefix) {
sortType = string(options.SortBy)
} else {
sortType = "newest"
}
}

// See the comment of issues_model.SearchOptions for the reason why we need to convert
Expand Down Expand Up @@ -68,7 +73,6 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
ExcludedLabelNames: nil,
IncludeMilestones: nil,
SortType: sortType,
IssueIDs: nil,
UpdatedAfterUnix: options.UpdatedAfterUnix.Value(),
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
PriorityRepoID: 0,
Expand Down
13 changes: 12 additions & 1 deletion modules/indexer/issues/dboptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
package issues

import (
"strings"

"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/indexer/issues/internal"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
)

func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions {
if opts.IssueIDs != nil {
setting.PanicInDevOrTesting("Indexer SearchOptions doesn't support IssueIDs")
}
searchOpt := &SearchOptions{
Keyword: keyword,
RepoIDs: opts.RepoIDs,
Expand Down Expand Up @@ -95,7 +102,11 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
// Unsupported sort type for search
fallthrough
default:
searchOpt.SortBy = SortByUpdatedDesc
if strings.HasPrefix(opts.SortType, issues_model.ScopeSortPrefix) {
searchOpt.SortBy = internal.SortBy(opts.SortType)
} else {
searchOpt.SortBy = SortByUpdatedDesc
}
}

return searchOpt
Expand Down
11 changes: 7 additions & 4 deletions modules/indexer/issues/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func InitIssueIndexer(syncReindex bool) {
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err)
}
case "db":
issueIndexer = db.NewIndexer()
issueIndexer = db.GetIndexer()
case "meilisearch":
issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName)
existed, err = issueIndexer.Init(ctx)
Expand Down Expand Up @@ -291,19 +291,22 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
// So if the user creates an issue and list issues immediately, the issue may not be listed because the indexer needs time to index the issue.
// Even worse, the external indexer like elastic search may not be available for a while,
// and the user may not be able to list issues completely until it is available again.
ix = db.NewIndexer()
ix = db.GetIndexer()
}

result, err := ix.Search(ctx, opts)
if err != nil {
return nil, 0, err
}
return SearchResultToIDSlice(result), result.Total, nil
}

func SearchResultToIDSlice(result *internal.SearchResult) []int64 {
ret := make([]int64, 0, len(result.Hits))
for _, hit := range result.Hits {
ret = append(ret, hit.ID)
}

return ret, result.Total, nil
return ret
}

// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
Expand Down
9 changes: 5 additions & 4 deletions modules/label/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")

// Label represents label information loaded from template
type Label struct {
Name string `yaml:"name"`
Color string `yaml:"color"`
Description string `yaml:"description,omitempty"`
Exclusive bool `yaml:"exclusive,omitempty"`
Name string `yaml:"name"`
Color string `yaml:"color"`
Description string `yaml:"description,omitempty"`
Exclusive bool `yaml:"exclusive,omitempty"`
ExclusiveOrder int `yaml:"exclusive_order,omitempty"`
}

// NormalizeColor normalizes a color string to a 6-character hex code
Expand Down
9 changes: 5 additions & 4 deletions modules/repository/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
labels := make([]*issues_model.Label, len(list))
for i := 0; i < len(list); i++ {
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
Description: list[i].Description,
Color: list[i].Color,
Name: list[i].Name,
Exclusive: list[i].Exclusive,
ExclusiveOrder: list[i].ExclusiveOrder,
Description: list[i].Description,
Color: list[i].Color,
}
if isOrg {
labels[i].OrgID = id
Expand Down
17 changes: 16 additions & 1 deletion modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,28 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
itemColor := "#" + hex.EncodeToString(itemBytes)
scopeColor := "#" + hex.EncodeToString(scopeBytes)

if label.ExclusiveOrder > 0 {
// <scope> | <label> | <order>
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right">%d</div>`+
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML,
label.ExclusiveOrder)
}

// <scope> | <label>
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML)
textColor, itemColor, itemHTML,
)
}

// RenderEmoji renders html text with emoji post processors
Expand Down
11 changes: 11 additions & 0 deletions options/label/Advanced.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,60 @@ labels:
description: Breaking change that won't be backward compatible
- name: "Reviewed/Duplicate"
exclusive: true
exclusive_order: 2
color: 616161
description: This issue or pull request already exists
- name: "Reviewed/Invalid"
exclusive: true
exclusive_order: 3
color: 546e7a
description: Invalid issue
- name: "Reviewed/Confirmed"
exclusive: true
exclusive_order: 1
color: 795548
description: Issue has been confirmed
- name: "Reviewed/Won't Fix"
exclusive: true
exclusive_order: 3
color: eeeeee
description: This issue won't be fixed
- name: "Status/Need More Info"
exclusive: true
exclusive_order: 2
color: 424242
description: Feedback is required to reproduce issue or to continue work
- name: "Status/Blocked"
exclusive: true
exclusive_order: 1
color: 880e4f
description: Something is blocking this issue or pull request
- name: "Status/Abandoned"
exclusive: true
exclusive_order: 3
color: "222222"
description: Somebody has started to work on this but abandoned work
- name: "Priority/Critical"
exclusive: true
exclusive_order: 1
color: b71c1c
description: The priority is critical
priority: critical
- name: "Priority/High"
exclusive: true
exclusive_order: 2
color: d32f2f
description: The priority is high
priority: high
- name: "Priority/Medium"
exclusive: true
exclusive_order: 3
color: e64a19
description: The priority is medium
priority: medium
- name: "Priority/Low"
exclusive: true
exclusive_order: 4
color: 4caf50
description: The priority is low
priority: low
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,8 @@ issues.label_archived_filter = Show archived labels
issues.label_archive_tooltip = Archived labels are excluded by default from the suggestions when searching by label.
issues.label_exclusive_desc = Name the label <code>scope/item</code> to make it mutually exclusive with other <code>scope/</code> labels.
issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request.
issues.label_exclusive_order = Sort Order
issues.label_exclusive_order_tooltip = Exclusive labels in the same scope will be sorted according to this numeric order.
issues.label_count = %d labels
issues.label_open_issues = %d open issues/pull requests
issues.label_edit = Edit
Expand Down
12 changes: 7 additions & 5 deletions routers/web/org/org_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ func NewLabel(ctx *context.Context) {
}

l := &issues_model.Label{
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Exclusive: form.Exclusive,
Description: form.Description,
Color: form.Color,
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Exclusive: form.Exclusive,
Description: form.Description,
Color: form.Color,
ExclusiveOrder: form.ExclusiveOrder,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
Expand All @@ -73,6 +74,7 @@ func UpdateLabel(ctx *context.Context) {

l.Name = form.Title
l.Exclusive = form.Exclusive
l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color
l.SetArchived(form.IsArchived)
Expand Down
Loading