mirror of
https://github.com/go-gitea/gitea
synced 2025-07-22 18:28:37 +00:00
feat: Add sorting by exclusive labels (issue priority) (#33206)
Fix #2616 This PR adds a new sort option for exclusive labels. For exclusive labels, a new property is exposed called "order", while in the UI options are populated automatically in the `Sort` column (see screenshot below) for each exclusive label scope. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
@@ -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,
|
||||
@@ -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
|
||||
|
@@ -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)
|
||||
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user