From 05a2a82a33519e9297c4ebd6690e116f4d3e46d7 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 2 May 2023 15:37:19 +0300 Subject: [PATCH] Add advanced labels with priority * Calculate issue priority when adding/removing labels --- models/fixtures/label.yml | 1 + models/issues/issue.go | 44 ++++++ models/issues/label.go | 31 ++++- models/issues/label_test.go | 125 +++++++++--------- models/migrations/migrations.go | 2 + models/migrations/v1_20/v256.go | 21 +++ modules/label/label.go | 68 +++++++++- modules/label/parser.go | 3 + modules/migration/label.go | 17 ++- modules/repository/init.go | 3 +- modules/structs/issue_label.go | 12 +- options/locale/locale_en-US.ini | 5 + routers/api/v1/org/label.go | 9 ++ routers/api/v1/repo/label.go | 14 ++ routers/web/org/org_labels.go | 6 +- routers/web/org/setting.go | 2 + routers/web/repo/issue_label.go | 7 +- services/convert/issue.go | 1 + services/convert/issue_test.go | 9 +- services/forms/repo_form.go | 3 +- services/migrations/gitea_downloader.go | 6 +- services/migrations/gitea_uploader.go | 3 +- services/migrations/gitlab.go | 32 +++++ services/repository/template.go | 3 +- .../repo/issue/labels/edit_delete_label.tmpl | 11 ++ templates/repo/issue/labels/label_list.tmpl | 4 +- templates/repo/issue/labels/label_new.tmpl | 11 ++ templates/swagger/v1_json.tmpl | 30 +++++ web_src/js/features/comp/LabelEdit.js | 1 + 29 files changed, 396 insertions(+), 88 deletions(-) create mode 100644 models/migrations/v1_20/v256.go 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 @@ +
+ +
+ +
+
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 325efd1da6e4d..ac69c7bbdefde 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -44,10 +44,10 @@
{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}} - {{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}} + {{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}} {{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}} {{else if $.PageIsOrgSettingsLabels}} - {{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}} + {{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}} {{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}} {{end}}
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index 41dced4a5bf8d..c905d33afcac5 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -34,6 +34,17 @@ +
+ +
+ +
+
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 51d123866de53..ef8834b2b18f1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -16418,6 +16418,16 @@ "name": { "type": "string", "x-go-name": "Name" + }, + "priority": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low" + ], + "x-go-name": "Priority" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -17365,6 +17375,16 @@ "name": { "type": "string", "x-go-name": "Name" + }, + "priority": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low" + ], + "x-go-name": "Priority" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -18810,6 +18830,16 @@ "type": "string", "x-go-name": "Name" }, + "priority": { + "type": "string", + "enum": [ + "critical", + "high", + "medium", + "low" + ], + "x-go-name": "Priority" + }, "url": { "type": "string", "x-go-name": "URL" diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js index 18676d25e6cc9..1fa1bb2f928dc 100644 --- a/web_src/js/features/comp/LabelEdit.js +++ b/web_src/js/features/comp/LabelEdit.js @@ -60,6 +60,7 @@ export function initCompLabelEdit(selector) { $('.edit-label .label-desc-input').val($(this).data('description')); $('.edit-label .color-picker').val($(this).data('color')); $('.edit-label .minicolors-swatch-color').css('background-color', $(this).data('color')); + $('.edit-label .label-priority-input').val($(this).data('priority')); $('.edit-label.modal').modal({ onApprove() {