diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml
index ab4d5ef944045..29a671dc55966 100644
--- a/models/fixtures/label.yml
+++ b/models/fixtures/label.yml
@@ -4,6 +4,7 @@
org_id: 0
name: label1
color: '#abcdef'
+ priority: high
exclusive: false
num_issues: 2
num_closed_issues: 0
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 8c173433f2546..f0e9b74eab908 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -232,6 +232,31 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
return nil
}
+// CalculatePriority calculates priority value of an issue based on its labels
+func (issue *Issue) CalculatePriority() {
+ // If no labels are set then set default priority
+ if issue.Labels == nil {
+ issue.Priority = 0
+ return
+ }
+ // Use only the labels that has priority set
+ p := -10000000
+ for _, label := range issue.Labels {
+ if label.Priority.IsEmpty() {
+ continue
+ }
+ if pv := label.Priority.Value(); p < pv {
+ p = pv
+ }
+ }
+ // If no label has priority use default
+ if p == -10000000 {
+ issue.Priority = 0
+ } else {
+ issue.Priority = p
+ }
+}
+
// LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 {
@@ -521,6 +546,11 @@ func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
return err
}
+ issue.Priority = 0
+ if err = UpdateIssueCols(ctx, issue, "priority"); err != nil {
+ return err
+ }
+
if err = committer.Commit(); err != nil {
return fmt.Errorf("Commit: %w", err)
}
@@ -634,6 +664,11 @@ func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (e
return err
}
+ issue.CalculatePriority()
+ if err = UpdateIssueCols(ctx, issue, "priority"); err != nil {
+ return err
+ }
+
return committer.Commit()
}
@@ -1020,6 +1055,15 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err)
}
}
+
+ opts.Issue.Labels = nil
+ if err = opts.Issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ if err = UpdateIssueCols(ctx, opts.Issue, "priority"); err != nil {
+ return err
+ }
}
if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil {
diff --git a/models/issues/label.go b/models/issues/label.go
index 35c649e8f24d9..1f9e645f06980 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -86,7 +86,8 @@ type Label struct {
Name string
Exclusive bool
Description string
- Color string `xorm:"VARCHAR(7)"`
+ Color string `xorm:"VARCHAR(7)"`
+ Priority label.Priority `xorm:"VARCHAR(20)"`
NumIssues int
NumClosedIssues int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
@@ -204,6 +205,9 @@ func NewLabel(ctx context.Context, l *Label) error {
if err != nil {
return err
}
+ if !l.Priority.IsValid() {
+ return fmt.Errorf("invalid priority: %s", l.Priority)
+ }
l.Color = color
return db.Insert(ctx, l)
@@ -222,6 +226,9 @@ func NewLabels(labels ...*Label) error {
if err != nil {
return err
}
+ if !l.Priority.IsValid() {
+ return fmt.Errorf("invalid priority: %s", l.Priority)
+ }
l.Color = color
if err := db.Insert(ctx, l); err != nil {
@@ -237,9 +244,12 @@ func UpdateLabel(l *Label) error {
if err != nil {
return err
}
+ if !l.Priority.IsValid() {
+ return fmt.Errorf("invalid priority: %s", l.Priority)
+ }
l.Color = color
- return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive")
+ return updateLabelCols(db.DefaultContext, l, "name", "exclusive", "color", "priority", "description")
}
// DeleteLabel delete a label
@@ -663,6 +673,11 @@ func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error
return err
}
+ issue.CalculatePriority()
+ if err = UpdateIssueCols(ctx, issue, "priority"); err != nil {
+ return err
+ }
+
return committer.Commit()
}
@@ -703,6 +718,11 @@ func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err e
return err
}
+ issue.CalculatePriority()
+ if err = UpdateIssueCols(ctx, issue, "priority"); err != nil {
+ return err
+ }
+
return committer.Commit()
}
@@ -741,7 +761,12 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use
}
issue.Labels = nil
- return issue.LoadLabels(ctx)
+ if err := issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ issue.CalculatePriority()
+ return UpdateIssueCols(ctx, issue, "priority")
}
// DeleteLabelsByRepoID deletes labels of some repository
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
index 1f6ce4f42ee78..861194539f526 100644
--- a/models/issues/label_test.go
+++ b/models/issues/label_test.go
@@ -11,33 +11,34 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/label"
"github.com/stretchr/testify/assert"
)
func TestLabel_CalOpenIssues(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- label.CalOpenIssues()
- assert.EqualValues(t, 2, label.NumOpenIssues)
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ l.CalOpenIssues()
+ assert.EqualValues(t, 2, l.NumOpenIssues)
}
func TestLabel_TextColor(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.False(t, label.UseLightTextColor())
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ assert.False(t, l.UseLightTextColor())
- label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
- assert.True(t, label.UseLightTextColor())
+ l = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
+ assert.True(t, l.UseLightTextColor())
}
func TestLabel_ExclusiveScope(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
- assert.Equal(t, "scope", label.ExclusiveScope())
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
+ assert.Equal(t, "scope", l.ExclusiveScope())
- label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 9})
- assert.Equal(t, "scope/subscope", label.ExclusiveScope())
+ l = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 9})
+ assert.Equal(t, "scope/subscope", l.ExclusiveScope())
}
func TestNewLabels(t *testing.T) {
@@ -53,21 +54,21 @@ func TestNewLabels(t *testing.T) {
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"}))
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"}))
- for _, label := range labels {
- unittest.AssertNotExistsBean(t, label)
+ for _, l := range labels {
+ unittest.AssertNotExistsBean(t, l)
}
assert.NoError(t, issues_model.NewLabels(labels...))
- for _, label := range labels {
- unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID))
+ for _, l := range labels {
+ unittest.AssertExistsAndLoadBean(t, l, unittest.Cond("id = ?", l.ID))
}
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
}
func TestGetLabelByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label, err := issues_model.GetLabelByID(db.DefaultContext, 1)
+ l, err := issues_model.GetLabelByID(db.DefaultContext, 1)
assert.NoError(t, err)
- assert.EqualValues(t, 1, label.ID)
+ assert.EqualValues(t, 1, l.ID)
_, err = issues_model.GetLabelByID(db.DefaultContext, unittest.NonexistentID)
assert.True(t, issues_model.IsErrLabelNotExist(err))
@@ -75,10 +76,10 @@ func TestGetLabelByID(t *testing.T) {
func TestGetLabelInRepoByName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1")
+ l, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1")
assert.NoError(t, err)
- assert.EqualValues(t, 1, label.ID)
- assert.Equal(t, "label1", label.Name)
+ assert.EqualValues(t, 1, l.ID)
+ assert.Equal(t, "label1", l.Name)
_, err = issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "")
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
@@ -113,9 +114,9 @@ func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) {
func TestGetLabelInRepoByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1)
+ l, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1)
assert.NoError(t, err)
- assert.EqualValues(t, 1, label.ID)
+ assert.EqualValues(t, 1, l.ID)
_, err = issues_model.GetLabelInRepoByID(db.DefaultContext, 1, -1)
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
@@ -140,8 +141,8 @@ func TestGetLabelsByRepoID(t *testing.T) {
labels, err := issues_model.GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{})
assert.NoError(t, err)
assert.Len(t, labels, len(expectedIssueIDs))
- for i, label := range labels {
- assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+ for i, l := range labels {
+ assert.EqualValues(t, expectedIssueIDs[i], l.ID)
}
}
testSuccess(1, "leastissues", []int64{2, 1})
@@ -154,10 +155,10 @@ func TestGetLabelsByRepoID(t *testing.T) {
func TestGetLabelInOrgByName(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3")
+ l, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3")
assert.NoError(t, err)
- assert.EqualValues(t, 3, label.ID)
- assert.Equal(t, "orglabel3", label.Name)
+ assert.EqualValues(t, 3, l.ID)
+ assert.Equal(t, "orglabel3", l.Name)
_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "")
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
@@ -198,9 +199,9 @@ func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
func TestGetLabelInOrgByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3)
+ l, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3)
assert.NoError(t, err)
- assert.EqualValues(t, 3, label.ID)
+ assert.EqualValues(t, 3, l.ID)
_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 3, -1)
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
@@ -231,8 +232,8 @@ func TestGetLabelsByOrgID(t *testing.T) {
labels, err := issues_model.GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{})
assert.NoError(t, err)
assert.Len(t, labels, len(expectedIssueIDs))
- for i, label := range labels {
- assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+ for i, l := range labels {
+ assert.EqualValues(t, expectedIssueIDs[i], l.ID)
}
}
testSuccess(3, "leastissues", []int64{3, 4})
@@ -265,34 +266,36 @@ func TestGetLabelsByIssueID(t *testing.T) {
func TestUpdateLabel(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
// make sure update wont overwrite it
update := &issues_model.Label{
- ID: label.ID,
- Color: "#ffff00",
+ ID: l.ID,
Name: "newLabelName",
- Description: label.Description,
Exclusive: false,
+ Color: "#ffff00",
+ Priority: label.Priority("high"),
+ Description: l.Description,
}
- label.Color = update.Color
- label.Name = update.Name
+ l.Color = update.Color
+ l.Name = update.Name
assert.NoError(t, issues_model.UpdateLabel(update))
newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.EqualValues(t, label.ID, newLabel.ID)
- assert.EqualValues(t, label.Color, newLabel.Color)
- assert.EqualValues(t, label.Name, newLabel.Name)
- assert.EqualValues(t, label.Description, newLabel.Description)
+ assert.EqualValues(t, l.ID, newLabel.ID)
+ assert.EqualValues(t, l.Color, newLabel.Color)
+ assert.EqualValues(t, l.Priority, newLabel.Priority)
+ assert.EqualValues(t, l.Name, newLabel.Name)
+ assert.EqualValues(t, l.Description, newLabel.Description)
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
}
func TestDeleteLabel(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
- unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID})
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ assert.NoError(t, issues_model.DeleteLabel(l.RepoID, l.ID))
+ unittest.AssertNotExistsBean(t, &issues_model.Label{ID: l.ID, RepoID: l.RepoID})
- assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
- unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID})
+ assert.NoError(t, issues_model.DeleteLabel(l.RepoID, l.ID))
+ unittest.AssertNotExistsBean(t, &issues_model.Label{ID: l.ID})
assert.NoError(t, issues_model.DeleteLabel(unittest.NonexistentID, unittest.NonexistentID))
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
@@ -307,26 +310,26 @@ func TestHasIssueLabel(t *testing.T) {
func TestNewIssueLabel(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// add new IssueLabel
- prevNumIssues := label.NumIssues
- assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
- unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
+ prevNumIssues := l.NumIssues
+ assert.NoError(t, issues_model.NewIssueLabel(issue, l, doer))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: l.ID})
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
Type: issues_model.CommentTypeLabel,
PosterID: doer.ID,
IssueID: issue.ID,
- LabelID: label.ID,
+ LabelID: l.ID,
Content: "1",
})
- label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
- assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
+ l = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
+ assert.EqualValues(t, prevNumIssues+1, l.NumIssues)
// re-add existing IssueLabel
- assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
+ assert.NoError(t, issues_model.NewIssueLabel(issue, l, doer))
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
}
@@ -391,12 +394,12 @@ func TestNewIssueLabels(t *testing.T) {
func TestDeleteIssueLabel(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testSuccess := func(labelID, issueID, doerID int64) {
- label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
+ l := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID})
- expectedNumIssues := label.NumIssues
- expectedNumClosedIssues := label.NumClosedIssues
+ expectedNumIssues := l.NumIssues
+ expectedNumClosedIssues := l.NumClosedIssues
if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) {
expectedNumIssues--
if issue.IsClosed {
@@ -407,7 +410,7 @@ func TestDeleteIssueLabel(t *testing.T) {
ctx, committer, err := db.TxContext(db.DefaultContext)
defer committer.Close()
assert.NoError(t, err)
- assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, label, doer))
+ assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, l, doer))
assert.NoError(t, committer.Commit())
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
@@ -417,9 +420,9 @@ func TestDeleteIssueLabel(t *testing.T) {
IssueID: issueID,
LabelID: labelID,
}, `content=""`)
- label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
- assert.EqualValues(t, expectedNumIssues, label.NumIssues)
- assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
+ l = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
+ assert.EqualValues(t, expectedNumIssues, l.NumIssues)
+ assert.EqualValues(t, expectedNumClosedIssues, l.NumClosedIssues)
}
testSuccess(1, 1, 2)
testSuccess(2, 5, 2)
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 1f1f43796cf4a..c79e16122b7df 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -489,6 +489,8 @@ var migrations = []Migration{
NewMigration("Add ActionTaskOutput table", v1_20.AddActionTaskOutputTable),
// v255 -> v256
NewMigration("Add ArchivedUnix Column", v1_20.AddArchivedUnixToRepository),
+ // v256 -> v257
+ NewMigration("Add labels priority", v1_20.AddLabelsPriority),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_20/v256.go b/models/migrations/v1_20/v256.go
new file mode 100644
index 0000000000000..97386640e90e6
--- /dev/null
+++ b/models/migrations/v1_20/v256.go
@@ -0,0 +1,21 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_20 //nolint
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func AddLabelsPriority(x *xorm.Engine) error {
+ type Label struct {
+ Priority string `xorm:"VARCHAR(20)"`
+ }
+
+ if err := x.Sync2(new(Label)); err != nil {
+ return fmt.Errorf("sync2: %w", err)
+ }
+ return nil
+}
diff --git a/modules/label/label.go b/modules/label/label.go
index d3ef0e1dc967a..2f7ae3b575263 100644
--- a/modules/label/label.go
+++ b/modules/label/label.go
@@ -6,18 +6,30 @@ package label
import (
"fmt"
"regexp"
+ "sort"
"strings"
)
// colorPattern is a regexp which can validate label color
var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
+// Priority represents label priority
+type Priority string
+
+var priorityValues = map[Priority]int{
+ "critical": 1000,
+ "high": 100,
+ "medium": 0,
+ "low": -100,
+}
+
// 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"`
+ Exclusive bool `yaml:"exclusive,omitempty"`
+ Color string `yaml:"color"`
+ Priority Priority `yaml:"priority,omitempty"`
+ Description string `yaml:"description,omitempty"`
}
// NormalizeColor normalizes a color string to a 6-character hex code
@@ -44,3 +56,51 @@ func NormalizeColor(color string) (string, error) {
return color, nil
}
+
+var priorities []Priority
+
+// Value returns numeric value for priority
+func (p Priority) Value() int {
+ v, ok := priorityValues[p]
+ if !ok {
+ return 0
+ }
+ return v
+}
+
+// Valid checks if priority is valid
+func (p Priority) IsValid() bool {
+ if p.IsEmpty() {
+ return true
+ }
+ _, ok := priorityValues[p]
+ return ok
+}
+
+// IsEmpty check if priority is not set
+func (p Priority) IsEmpty() bool {
+ return len(p) == 0
+}
+
+// GetPriorities returns list of priorities
+func GetPriorities() []Priority {
+ return priorities
+}
+
+func init() {
+ type kv struct {
+ Key Priority
+ Value int
+ }
+ var ss []kv
+ for k, v := range priorityValues {
+ ss = append(ss, kv{k, v})
+ }
+ sort.Slice(ss, func(i, j int) bool {
+ return ss[i].Value > ss[j].Value
+ })
+ priorities = make([]Priority, len(priorityValues))
+ for i, kv := range ss {
+ priorities[i] = kv.Key
+ }
+}
diff --git a/modules/label/parser.go b/modules/label/parser.go
index 511bac823ff8d..ccd083778eea8 100644
--- a/modules/label/parser.go
+++ b/modules/label/parser.go
@@ -59,6 +59,9 @@ func parseYamlFormat(fileName string, data []byte) ([]*Label, error) {
if len(l.Name) == 0 || len(l.Color) == 0 {
return nil, ErrTemplateLoad{fileName, errors.New("label name and color are required fields")}
}
+ if !l.Priority.IsValid() {
+ return nil, ErrTemplateLoad{fileName, fmt.Errorf("invalid priority: %s", l.Priority)}
+ }
color, err := NormalizeColor(l.Color)
if err != nil {
return nil, ErrTemplateLoad{fileName, fmt.Errorf("bad HTML color code '%s' in label: %s", l.Color, l.Name)}
diff --git a/modules/migration/label.go b/modules/migration/label.go
index 4927be3c0bd11..d478021590e6f 100644
--- a/modules/migration/label.go
+++ b/modules/migration/label.go
@@ -4,10 +4,25 @@
package migration
+// LabelPriority represents the priority type of a label
+type LabelPriority string
+
+const (
+ // LabelPriorityLow represents the low priority of a label
+ LabelPriorityLow LabelPriority = "low"
+ // LabelPriorityMedium represents the medium priority of a label
+ LabelPriorityMedium LabelPriority = "medium"
+ // LabelPriorityHigh represents the high priority of a label
+ LabelPriorityHigh LabelPriority = "high"
+ // LabelPriorityCritical represents the critical priority of a label
+ LabelPriorityCritical LabelPriority = "critical"
+)
+
// Label defines a standard label information
type Label struct {
Name string `json:"name"`
+ Exclusive bool `json:"exclusive"`
Color string `json:"color"`
+ Priority LabelPriority
Description string `json:"description"`
- Exclusive bool `json:"exclusive"`
}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index cb353f24968a7..658ddbd3831bd 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -367,8 +367,9 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
- Description: list[i].Description,
Color: list[i].Color,
+ Priority: list[i].Priority,
+ Description: list[i].Description,
}
if isOrg {
labels[i].OrgID = id
diff --git a/modules/structs/issue_label.go b/modules/structs/issue_label.go
index 5bb6cc3b845b5..4248f873021be 100644
--- a/modules/structs/issue_label.go
+++ b/modules/structs/issue_label.go
@@ -12,7 +12,9 @@ type Label struct {
// example: false
Exclusive bool `json:"exclusive"`
// example: 00aabb
- Color string `json:"color"`
+ Color string `json:"color"`
+ // enum: critical,high,medium,low
+ Priority string `json:"priority"`
Description string `json:"description"`
URL string `json:"url"`
}
@@ -25,7 +27,9 @@ type CreateLabelOption struct {
Exclusive bool `json:"exclusive"`
// required:true
// example: #00aabb
- Color string `json:"color" binding:"Required"`
+ Color string `json:"color" binding:"Required"`
+ // enum: critical,high,medium,low
+ Priority string `json:"priority"`
Description string `json:"description"`
}
@@ -35,7 +39,9 @@ type EditLabelOption struct {
// example: false
Exclusive *bool `json:"exclusive"`
// example: #00aabb
- Color *string `json:"color"`
+ Color *string `json:"color"`
+ // enum: critical,high,medium,low
+ Priority *string `json:"priority"`
Description *string `json:"description"`
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a2f7f9c5e0e5e..503698ede2bce 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1446,6 +1446,7 @@ issues.save = Save
issues.label_title = Name
issues.label_description = Description
issues.label_color = Color
+issues.label_priority = Priority
issues.label_exclusive = Exclusive
issues.label_exclusive_desc = Name the label scope/item
to make it mutually exclusive with other scope/
labels.
issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request.
@@ -1457,6 +1458,10 @@ issues.label_modify = Edit Label
issues.label_deletion = Delete Label
issues.label_deletion_desc = Deleting a label removes it from all issues. Continue?
issues.label_deletion_success = The label has been deleted.
+issues.label_priority_critical = Critical
+issues.label_priority_high = High
+issues.label_priority_medium = Medium
+issues.label_priority_low = Low
issues.label.filter_sort.alphabetically = Alphabetically
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
issues.label.filter_sort.by_size = Smallest size
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index 183c1e6cc8a8c..13873540e362f 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -4,6 +4,7 @@
package org
import (
+ "fmt"
"net/http"
"strconv"
"strings"
@@ -95,6 +96,7 @@ func CreateLabel(ctx *context.APIContext) {
Name: form.Name,
Exclusive: form.Exclusive,
Color: form.Color,
+ Priority: label.Priority(form.Priority),
OrgID: ctx.Org.Organization.ID,
Description: form.Description,
}
@@ -206,6 +208,13 @@ func EditLabel(ctx *context.APIContext) {
}
l.Color = color
}
+ if form.Priority != nil {
+ l.Priority = label.Priority(*form.Priority)
+ if !l.Priority.IsValid() {
+ ctx.Error(http.StatusUnprocessableEntity, "Priority", fmt.Errorf("unknown priority: %s", l.Priority))
+ return
+ }
+ }
if form.Description != nil {
l.Description = *form.Description
}
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 6cb231f596c8b..ac7f9176df57e 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -5,6 +5,7 @@
package repo
import (
+ "fmt"
"net/http"
"strconv"
@@ -152,10 +153,16 @@ func CreateLabel(ctx *context.APIContext) {
}
form.Color = color
+ if !label.Priority(form.Priority).IsValid() {
+ ctx.Error(http.StatusUnprocessableEntity, "Priority", fmt.Errorf("unknown priority: %s", form.Priority))
+ return
+ }
+
l := &issues_model.Label{
Name: form.Name,
Exclusive: form.Exclusive,
Color: form.Color,
+ Priority: label.Priority(form.Priority),
RepoID: ctx.Repo.Repository.ID,
Description: form.Description,
}
@@ -228,6 +235,13 @@ func EditLabel(ctx *context.APIContext) {
}
l.Color = color
}
+ if form.Priority != nil {
+ l.Priority = label.Priority(*form.Priority)
+ if !l.Priority.IsValid() {
+ ctx.Error(http.StatusUnprocessableEntity, "Priority", fmt.Errorf("unknown priority: %s", *form.Priority))
+ return
+ }
+ }
if form.Description != nil {
l.Description = *form.Description
}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index 9ce05680d7bd2..68c08000ff03c 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -47,8 +47,9 @@ func NewLabel(ctx *context.Context) {
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Exclusive: form.Exclusive,
- Description: form.Description,
Color: form.Color,
+ Priority: label.Priority(form.Priority),
+ Description: form.Description,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
@@ -73,8 +74,9 @@ func UpdateLabel(ctx *context.Context) {
l.Name = form.Title
l.Exclusive = form.Exclusive
- l.Description = form.Description
l.Color = form.Color
+ l.Priority = label.Priority(form.Priority)
+ l.Description = form.Description
if err := issues_model.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 4111b13531510..d8ee3c07611c4 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -243,5 +244,6 @@ func Labels(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsLabels"] = true
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
+ ctx.Data["LabelPriorities"] = label.GetPriorities()
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 002acbf1d3c85..3be6c7c3087cb 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -29,6 +29,7 @@ func Labels(ctx *context.Context) {
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsLabels"] = true
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
+ ctx.Data["LabelPriorities"] = label.GetPriorities()
ctx.HTML(http.StatusOK, tplLabels)
}
@@ -114,8 +115,9 @@ func NewLabel(ctx *context.Context) {
RepoID: ctx.Repo.Repository.ID,
Name: form.Title,
Exclusive: form.Exclusive,
- Description: form.Description,
Color: form.Color,
+ Priority: label.Priority(form.Priority),
+ Description: form.Description,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
@@ -140,8 +142,9 @@ func UpdateLabel(ctx *context.Context) {
l.Name = form.Title
l.Exclusive = form.Exclusive
- l.Description = form.Description
l.Color = form.Color
+ l.Priority = label.Priority(form.Priority)
+ l.Description = form.Description
if err := issues_model.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
diff --git a/services/convert/issue.go b/services/convert/issue.go
index 6d31a123bd9fc..f00fa045c346e 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -188,6 +188,7 @@ func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_m
Name: label.Name,
Exclusive: label.Exclusive,
Color: strings.TrimLeft(label.Color, "#"),
+ Priority: string(label.Priority),
Description: label.Description,
}
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
index 4d780f3f00905..1f2da272fc469 100644
--- a/services/convert/issue_test.go
+++ b/services/convert/issue_test.go
@@ -23,10 +23,11 @@ func TestLabel_ToLabel(t *testing.T) {
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID})
assert.Equal(t, &api.Label{
- ID: label.ID,
- Name: label.Name,
- Color: "abcdef",
- URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
+ ID: label.ID,
+ Name: label.Name,
+ Color: "abcdef",
+ Priority: "high",
+ URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
}, ToLabel(label, repo, nil))
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 41d7dc7d2b0a9..be07d15ab15ea 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -566,8 +566,9 @@ type CreateLabelForm struct {
ID int64
Title string `binding:"Required;MaxSize(50)" locale:"repo.issues.label_title"`
Exclusive bool `form:"exclusive"`
- Description string `binding:"MaxSize(200)" locale:"repo.issues.label_description"`
Color string `binding:"Required;MaxSize(7)" locale:"repo.issues.label_color"`
+ Priority string `binding:"MaxSize(20);In(critical,high,medium,low)" locale:"repo.issues.label_priority"`
+ Description string `binding:"MaxSize(200)" locale:"repo.issues.label_description"`
}
// Validate validates the fields
diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go
index 470090b5010a5..0e450274eb14a 100644
--- a/services/migrations/gitea_downloader.go
+++ b/services/migrations/gitea_downloader.go
@@ -230,8 +230,10 @@ func (g *GiteaDownloader) GetMilestones() ([]*base.Milestone, error) {
func (g *GiteaDownloader) convertGiteaLabel(label *gitea_sdk.Label) *base.Label {
return &base.Label{
- Name: label.Name,
- Color: label.Color,
+ Name: label.Name,
+ Color: label.Color,
+ // TODO: Add priority migration once SDK has support for it
+ // Priority: label.Priority,
Description: label.Description,
}
}
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 0eb34b5fe562f..5ae5969eea386 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -230,8 +230,9 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
RepoID: g.repo.ID,
Name: l.Name,
Exclusive: l.Exclusive,
- Description: l.Description,
Color: l.Color,
+ Priority: label.Priority(l.Priority),
+ Description: l.Description,
})
}
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 8034869a4ae4b..2096404fd7cf0 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -269,6 +269,34 @@ func (g *GitlabDownloader) normalizeColor(val string) string {
return val
}
+func (g *GitlabDownloader) resolvePriorityMap(ls []*gitlab.Label) map[int]base.LabelPriority {
+ priorities := make(map[int]base.LabelPriority)
+ var max, min int
+ // Find max and min priorities
+ for _, l := range ls {
+ if max < l.Priority {
+ max = l.Priority
+ }
+ if min > l.Priority || min == 0 {
+ min = l.Priority
+ }
+ }
+ // Create map
+ for _, l := range ls {
+ if l.Priority == 0 {
+ continue
+ }
+ if l.Priority == max {
+ priorities[l.Priority] = base.LabelPriorityCritical
+ } else if l.Priority == min {
+ priorities[l.Priority] = base.LabelPriorityMedium
+ } else {
+ priorities[l.Priority] = base.LabelPriorityHigh
+ }
+ }
+ return priorities
+}
+
// GetLabels returns labels
func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
perPage := g.maxPerPage
@@ -281,12 +309,16 @@ func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
if err != nil {
return nil, err
}
+ pm := g.resolvePriorityMap(ls)
for _, label := range ls {
baseLabel := &base.Label{
Name: label.Name,
Color: g.normalizeColor(label.Color),
Description: label.Description,
}
+ if label.Priority != 0 {
+ baseLabel.Priority = pm[label.Priority]
+ }
labels = append(labels, baseLabel)
}
if len(ls) < perPage {
diff --git a/services/repository/template.go b/services/repository/template.go
index 42174d095b035..014914ea18a94 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -32,8 +32,9 @@ func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_m
RepoID: generateRepo.ID,
Name: templateLabel.Name,
Exclusive: templateLabel.Exclusive,
- Description: templateLabel.Description,
Color: templateLabel.Color,
+ Priority: templateLabel.Priority,
+ Description: templateLabel.Description,
})
}
return db.Insert(ctx, newLabels)
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index b09e9173de4e5..9a25b088f4b86 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -49,6 +49,17 @@
+