summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--integrations/api_issue_label_test.go71
-rw-r--r--integrations/api_issue_test.go65
-rw-r--r--models/error.go35
-rw-r--r--models/fixtures/issue_label.yml5
-rw-r--r--models/fixtures/label.yml20
-rw-r--r--models/issue.go4
-rw-r--r--models/issue_label.go372
-rw-r--r--models/issue_label_test.go111
-rw-r--r--models/issue_list_test.go1
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v135.go22
-rw-r--r--modules/repository/create.go2
-rw-r--r--modules/test/context_tests.go4
-rw-r--r--options/locale/locale_en-US.ini5
-rw-r--r--routers/api/v1/api.go7
-rw-r--r--routers/api/v1/org/label.go237
-rw-r--r--routers/api/v1/repo/issue.go7
-rw-r--r--routers/api/v1/repo/issue_label.go8
-rw-r--r--routers/api/v1/repo/label.go4
-rw-r--r--routers/org/org_labels.go106
-rw-r--r--routers/org/setting.go12
-rw-r--r--routers/repo/compare.go1
-rw-r--r--routers/repo/issue.go36
-rw-r--r--routers/repo/issue_label.go41
-rw-r--r--routers/routes/routes.go8
-rw-r--r--services/issue/label.go5
-rw-r--r--templates/org/settings/labels.tmpl29
-rw-r--r--templates/org/settings/navbar.tmpl3
-rw-r--r--templates/repo/issue/labels.tmpl170
-rw-r--r--templates/repo/issue/labels/edit_delete_label.tmpl59
-rw-r--r--templates/repo/issue/labels/label_list.tmpl97
-rw-r--r--templates/repo/issue/labels/label_load_template.tmpl30
-rw-r--r--templates/repo/issue/labels/label_new.tmpl27
-rw-r--r--templates/repo/issue/new_form.tmpl10
-rw-r--r--templates/repo/issue/view_content/sidebar.tmpl10
-rw-r--r--templates/swagger/v1_json.tmpl183
-rw-r--r--web_src/js/index.js69
-rw-r--r--web_src/less/_organization.less41
-rw-r--r--web_src/less/_repository.less23
39 files changed, 1627 insertions, 315 deletions
diff --git a/integrations/api_issue_label_test.go b/integrations/api_issue_label_test.go
index 6cdb3a0dad..ddcfdd6615 100644
--- a/integrations/api_issue_label_test.go
+++ b/integrations/api_issue_label_test.go
@@ -134,3 +134,74 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
models.AssertCount(t, &models.IssueLabel{IssueID: issue.ID}, 1)
models.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
}
+
+func TestAPIModifyOrgLabels(t *testing.T) {
+ assert.NoError(t, models.LoadFixtures())
+
+ repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)
+ owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+ user := "user1"
+ session := loginUser(t, user)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)
+
+ // CreateLabel
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 1",
+ Color: "abcdef",
+ Description: "test label",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ apiLabel := new(api.Label)
+ DecodeJSON(t, resp, &apiLabel)
+ dbLabel := models.AssertExistsAndLoadBean(t, &models.Label{ID: apiLabel.ID, OrgID: owner.ID}).(*models.Label)
+ assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 2",
+ Color: "#123456",
+ Description: "jet another test label",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "WrongTestL",
+ Color: "#12345g",
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ //ListLabels
+ req = NewRequest(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiLabels []*api.Label
+ DecodeJSON(t, resp, &apiLabels)
+ assert.Len(t, apiLabels, 4)
+
+ //GetLabel
+ singleURLStr := fmt.Sprintf("/api/v1/orgs/%s/labels/%d?token=%s", owner.Name, dbLabel.ID, token)
+ req = NewRequest(t, "GET", singleURLStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ //EditLabel
+ newName := "LabelNewName"
+ newColor := "09876a"
+ newColorWrong := "09g76a"
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Name: &newName,
+ Color: &newColor,
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, newColor, apiLabel.Color)
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Color: &newColorWrong,
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ //DeleteLabel
+ req = NewRequest(t, "DELETE", singleURLStr)
+ resp = session.MakeRequest(t, req, http.StatusNoContent)
+
+}
diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go
index 30fbda173c..2a6f137747 100644
--- a/integrations/api_issue_test.go
+++ b/integrations/api_issue_test.go
@@ -130,7 +130,7 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, title, issueAfter.Title)
}
-func TestAPISearchIssue(t *testing.T) {
+func TestAPISearchIssues(t *testing.T) {
defer prepareTestEnv(t)()
session := loginUser(t, "user2")
@@ -173,3 +173,66 @@ func TestAPISearchIssue(t *testing.T) {
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 1)
}
+
+func TestAPISearchIssuesWithLabels(t *testing.T) {
+ defer prepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ link, _ := url.Parse("/api/v1/repos/issues/search")
+ req := NewRequest(t, "GET", link.String())
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiIssues []*api.Issue
+ DecodeJSON(t, resp, &apiIssues)
+
+ assert.Len(t, apiIssues, 9)
+
+ query := url.Values{}
+ query.Add("token", token)
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 9)
+
+ query.Add("labels", "label1")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // multiple labels
+ query.Set("labels", "label1,label2")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // an org label
+ query.Set("labels", "orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 1)
+
+ // org and repo label
+ query.Set("labels", "label2,orglabel4")
+ query.Add("state", "all")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // org and repo label which share the same issue
+ query.Set("labels", "label1,orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+}
diff --git a/models/error.go b/models/error.go
index f53479fac8..f54df37330 100644
--- a/models/error.go
+++ b/models/error.go
@@ -1502,10 +1502,41 @@ func (err ErrTrackedTimeNotExist) Error() string {
// |_______ (____ /___ /\___ >____/
// \/ \/ \/ \/
+// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
+type ErrRepoLabelNotExist struct {
+ LabelID int64
+ RepoID int64
+}
+
+// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
+func IsErrRepoLabelNotExist(err error) bool {
+ _, ok := err.(ErrRepoLabelNotExist)
+ return ok
+}
+
+func (err ErrRepoLabelNotExist) Error() string {
+ return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
+}
+
+// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
+type ErrOrgLabelNotExist struct {
+ LabelID int64
+ OrgID int64
+}
+
+// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
+func IsErrOrgLabelNotExist(err error) bool {
+ _, ok := err.(ErrOrgLabelNotExist)
+ return ok
+}
+
+func (err ErrOrgLabelNotExist) Error() string {
+ return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
+}
+
// ErrLabelNotExist represents a "LabelNotExist" kind of error.
type ErrLabelNotExist struct {
LabelID int64
- RepoID int64
}
// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
@@ -1515,7 +1546,7 @@ func IsErrLabelNotExist(err error) bool {
}
func (err ErrLabelNotExist) Error() string {
- return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
+ return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
}
// _____ .__.__ __
diff --git a/models/fixtures/issue_label.yml b/models/fixtures/issue_label.yml
index 49d5a95d02..f4ecb1f923 100644
--- a/models/fixtures/issue_label.yml
+++ b/models/fixtures/issue_label.yml
@@ -12,3 +12,8 @@
id: 3
issue_id: 2
label_id: 1
+
+-
+ id: 4
+ issue_id: 2
+ label_id: 4
diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml
index 5336342b1c..3ad82eebed 100644
--- a/models/fixtures/label.yml
+++ b/models/fixtures/label.yml
@@ -1,6 +1,7 @@
-
id: 1
repo_id: 1
+ org_id: 0
name: label1
color: '#abcdef'
num_issues: 2
@@ -9,7 +10,26 @@
-
id: 2
repo_id: 1
+ org_id: 0
name: label2
color: '#000000'
num_issues: 1
num_closed_issues: 1
+-
+ id: 3
+ repo_id: 0
+ org_id: 3
+ name: orglabel3
+ color: '#abcdef'
+ num_issues: 0
+ num_closed_issues: 0
+
+-
+ id: 4
+ repo_id: 0
+ org_id: 3
+ name: orglabel4
+ color: '#000000'
+ num_issues: 1
+ num_closed_issues: 0
+
diff --git a/models/issue.go b/models/issue.go
index 8aa02873a1..db8991095d 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -459,7 +459,7 @@ func (issue *Issue) ClearLabels(doer *User) (err error) {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
- return ErrLabelNotExist{}
+ return ErrRepoLabelNotExist{}
}
if err = issue.clearLabels(sess, doer); err != nil {
@@ -894,7 +894,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
for _, label := range labels {
// Silently drop invalid labels.
- if label.RepoID != opts.Repo.ID {
+ if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID {
continue
}
diff --git a/models/issue_label.go b/models/issue_label.go
index 3b516a7aed..cb9c307e2b 100644
--- a/models/issue_label.go
+++ b/models/issue_label.go
@@ -21,18 +21,20 @@ var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Label represents a label of repository for issues.
type Label struct {
- ID int64 `xorm:"pk autoincr"`
- RepoID int64 `xorm:"INDEX"`
- Name string
- Description string
- Color string `xorm:"VARCHAR(7)"`
- NumIssues int
- NumClosedIssues int
- NumOpenIssues int `xorm:"-"`
- IsChecked bool `xorm:"-"`
- QueryString string `xorm:"-"`
- IsSelected bool `xorm:"-"`
- IsExcluded bool `xorm:"-"`
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"`
+ OrgID int64 `xorm:"INDEX"`
+ Name string
+ Description string
+ Color string `xorm:"VARCHAR(7)"`
+ NumIssues int
+ NumClosedIssues int
+ NumOpenIssues int `xorm:"-"`
+ NumOpenRepoIssues int64 `xorm:"-"`
+ IsChecked bool `xorm:"-"`
+ QueryString string `xorm:"-"`
+ IsSelected bool `xorm:"-"`
+ IsExcluded bool `xorm:"-"`
}
// GetLabelTemplateFile loads the label template file by given name,
@@ -79,11 +81,26 @@ func GetLabelTemplateFile(name string) ([][3]string, error) {
return list, nil
}
-// CalOpenIssues calculates the open issues of label.
+// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
func (label *Label) CalOpenIssues() {
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
}
+// CalOpenOrgIssues calculates the open issues of a label for a specific repo
+func (label *Label) CalOpenOrgIssues(repoID, labelID int64) {
+ repoIDs := []int64{repoID}
+ labelIDs := []int64{labelID}
+
+ counts, _ := CountIssuesByRepo(&IssuesOptions{
+ RepoIDs: repoIDs,
+ LabelIDs: labelIDs,
+ })
+
+ for _, count := range counts {
+ label.NumOpenRepoIssues += count
+ }
+}
+
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
var labelQuerySlice []string
@@ -106,6 +123,16 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64)
label.QueryString = strings.Join(labelQuerySlice, ",")
}
+// BelongsToOrg returns true if label is an organization label
+func (label *Label) BelongsToOrg() bool {
+ return label.OrgID > 0
+}
+
+// BelongsToRepo returns true if label is a repository label
+func (label *Label) BelongsToRepo() bool {
+ return label.RepoID > 0
+}
+
// ForegroundColor calculates the text color for labels based
// on their background color.
func (label *Label) ForegroundColor() template.CSS {
@@ -126,6 +153,12 @@ func (label *Label) ForegroundColor() template.CSS {
return template.CSS("#000")
}
+// .____ ___. .__
+// | | _____ \_ |__ ____ | |
+// | | \__ \ | __ \_/ __ \| |
+// | |___ / __ \| \_\ \ ___/| |__
+// >_______ (____ /___ /\___ >____/
+
func loadLabels(labelTemplate string) ([]string, error) {
list, err := GetLabelTemplateFile(labelTemplate)
if err != nil {
@@ -145,7 +178,7 @@ func LoadLabelsFormatted(labelTemplate string) (string, error) {
return strings.Join(labels, ", "), err
}
-func initializeLabels(e Engine, repoID int64, labelTemplate string) error {
+func initializeLabels(e Engine, id int64, labelTemplate string, isOrg bool) error {
list, err := GetLabelTemplateFile(labelTemplate)
if err != nil {
return ErrIssueLabelTemplateLoad{labelTemplate, err}
@@ -154,11 +187,15 @@ func initializeLabels(e Engine, repoID int64, labelTemplate string) error {
labels := make([]*Label, len(list))
for i := 0; i < len(list); i++ {
labels[i] = &Label{
- RepoID: repoID,
Name: list[i][0],
Description: list[i][2],
Color: list[i][1],
}
+ if isOrg {
+ labels[i].OrgID = id
+ } else {
+ labels[i].RepoID = id
+ }
}
for _, label := range labels {
if err = newLabel(e, label); err != nil {
@@ -169,8 +206,8 @@ func initializeLabels(e Engine, repoID int64, labelTemplate string) error {
}
// InitializeLabels adds a label set to a repository using a template
-func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string) error {
- return initializeLabels(ctx.e, repoID, labelTemplate)
+func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string, isOrg bool) error {
+ return initializeLabels(ctx.e, repoID, labelTemplate, isOrg)
}
func newLabel(e Engine, label *Label) error {
@@ -178,7 +215,7 @@ func newLabel(e Engine, label *Label) error {
return err
}
-// NewLabel creates a new label for a repository
+// NewLabel creates a new label
func NewLabel(label *Label) error {
if !LabelColorPattern.MatchString(label.Color) {
return fmt.Errorf("bad color code: %s", label.Color)
@@ -186,7 +223,7 @@ func NewLabel(label *Label) error {
return newLabel(x, label)
}
-// NewLabels creates new labels for a repository.
+// NewLabels creates new labels
func NewLabels(labels ...*Label) error {
sess := x.NewSession()
defer sess.Close()
@@ -204,12 +241,98 @@ func NewLabels(labels ...*Label) error {
return sess.Commit()
}
+// UpdateLabel updates label information.
+func UpdateLabel(l *Label) error {
+ if !LabelColorPattern.MatchString(l.Color) {
+ return fmt.Errorf("bad color code: %s", l.Color)
+ }
+ return updateLabel(x, l)
+}
+
+// DeleteLabel delete a label
+func DeleteLabel(id, labelID int64) error {
+
+ label, err := GetLabelByID(labelID)
+ if err != nil {
+ if IsErrLabelNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if label.BelongsToOrg() && label.OrgID != id {
+ return nil
+ }
+ if label.BelongsToRepo() && label.RepoID != id {
+ return nil
+ }
+
+ if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
+ return err
+ } else if _, err = sess.
+ Where("label_id = ?", labelID).
+ Delete(new(IssueLabel)); err != nil {
+ return err
+ }
+
+ // delete comments about now deleted label_id
+ if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// getLabelByID returns a label by label id
+func getLabelByID(e Engine, labelID int64) (*Label, error) {
+ if labelID <= 0 {
+ return nil, ErrLabelNotExist{labelID}
+ }
+
+ l := &Label{
+ ID: labelID,
+ }
+ has, err := e.Get(l)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrLabelNotExist{l.ID}
+ }
+ return l, nil
+}
+
+// GetLabelByID returns a label by given ID.
+func GetLabelByID(id int64) (*Label, error) {
+ return getLabelByID(x, id)
+}
+
+// GetLabelsByIDs returns a list of labels by IDs
+func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
+ labels := make([]*Label, 0, len(labelIDs))
+ return labels, x.Table("label").
+ In("id", labelIDs).
+ Asc("name").
+ Cols("id").
+ Find(&labels)
+}
+
+// __________ .__ __
+// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
+// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
+// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
+// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
+// \/ \/|__| \/ \/
+
// getLabelInRepoByName returns a label by Name in given repository.
-// If pass repoID as 0, then ORM will ignore limitation of repository
-// and can return arbitrary label with any valid ID.
func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
- if len(labelName) == 0 {
- return nil, ErrLabelNotExist{0, repoID}
+ if len(labelName) == 0 || repoID <= 0 {
+ return nil, ErrRepoLabelNotExist{0, repoID}
}
l := &Label{
@@ -220,17 +343,15 @@ func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, err
if err != nil {
return nil, err
} else if !has {
- return nil, ErrLabelNotExist{0, l.RepoID}
+ return nil, ErrRepoLabelNotExist{0, l.RepoID}
}
return l, nil
}
// getLabelInRepoByID returns a label by ID in given repository.
-// If pass repoID as 0, then ORM will ignore limitation of repository
-// and can return arbitrary label with any valid ID.
func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
- if labelID <= 0 {
- return nil, ErrLabelNotExist{labelID, repoID}
+ if labelID <= 0 || repoID <= 0 {
+ return nil, ErrRepoLabelNotExist{labelID, repoID}
}
l := &Label{
@@ -241,16 +362,11 @@ func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
if err != nil {
return nil, err
} else if !has {
- return nil, ErrLabelNotExist{l.ID, l.RepoID}
+ return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
}
return l, nil
}
-// GetLabelByID returns a label by given ID.
-func GetLabelByID(id int64) (*Label, error) {
- return getLabelInRepoByID(x, 0, id)
-}
-
// GetLabelInRepoByName returns a label by name in given repository.
func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) {
return getLabelInRepoByName(x, repoID, labelName)
@@ -280,19 +396,6 @@ func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
GroupBy("issue_label.issue_id")
}
-// GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
-// repositories.
-// it silently ignores label names that do not belong to the repository.
-func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
- labelIDs := make([]int64, 0, len(labelNames))
- return labelIDs, x.Table("label").
- In("repo_id", repoIDs).
- In("name", labelNames).
- Asc("name").
- Cols("id").
- Find(&labelIDs)
-}
-
// GetLabelInRepoByID returns a label by ID in given repository.
func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
return getLabelInRepoByID(x, repoID, labelID)
@@ -310,6 +413,9 @@ func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
}
func getLabelsByRepoID(e Engine, repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
+ if repoID <= 0 {
+ return nil, ErrRepoLabelNotExist{0, repoID}
+ }
labels := make([]*Label, 0, 10)
sess := e.Where("repo_id = ?", repoID)
@@ -336,6 +442,138 @@ func GetLabelsByRepoID(repoID int64, sortType string, listOptions ListOptions) (
return getLabelsByRepoID(x, repoID, sortType, listOptions)
}
+// ________
+// \_____ \_______ ____
+// / | \_ __ \/ ___\
+// / | \ | \/ /_/ >
+// \_______ /__| \___ /
+// \/ /_____/
+
+// getLabelInOrgByName returns a label by Name in given organization
+func getLabelInOrgByName(e Engine, orgID int64, labelName string) (*Label, error) {
+ if len(labelName) == 0 || orgID <= 0 {
+ return nil, ErrOrgLabelNotExist{0, orgID}
+ }
+
+ l := &Label{
+ Name: labelName,
+ OrgID: orgID,
+ }
+ has, err := e.Get(l)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrOrgLabelNotExist{0, l.OrgID}
+ }
+ return l, nil
+}
+
+// getLabelInOrgByID returns a label by ID in given organization.
+func getLabelInOrgByID(e Engine, orgID, labelID int64) (*Label, error) {
+ if labelID <= 0 || orgID <= 0 {
+ return nil, ErrOrgLabelNotExist{labelID, orgID}
+ }
+
+ l := &Label{
+ ID: labelID,
+ OrgID: orgID,
+ }
+ has, err := e.Get(l)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
+ }
+ return l, nil
+}
+
+// GetLabelInOrgByName returns a label by name in given organization.
+func GetLabelInOrgByName(orgID int64, labelName string) (*Label, error) {
+ return getLabelInOrgByName(x, orgID, labelName)
+}
+
+// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
+// organization.
+func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) {
+ if orgID <= 0 {
+ return nil, ErrOrgLabelNotExist{0, orgID}
+ }
+ labelIDs := make([]int64, 0, len(labelNames))
+
+ return labelIDs, x.Table("label").
+ Where("org_id = ?", orgID).
+ In("name", labelNames).
+ Asc("name").
+ Cols("id").
+ Find(&labelIDs)
+}
+
+// GetLabelIDsInOrgsByNames returns a list of labelIDs by names in one of the given
+// organization.
+// it silently ignores label names that do not belong to the organization.
+func GetLabelIDsInOrgsByNames(orgIDs []int64, labelNames []string) ([]int64, error) {
+ labelIDs := make([]int64, 0, len(labelNames))
+ return labelIDs, x.Table("label").
+ In("org_id", orgIDs).
+ In("name", labelNames).
+ Asc("name").
+ Cols("id").
+ Find(&labelIDs)
+}
+
+// GetLabelInOrgByID returns a label by ID in given organization.
+func GetLabelInOrgByID(orgID, labelID int64) (*Label, error) {
+ return getLabelInOrgByID(x, orgID, labelID)
+}
+
+// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
+// it silently ignores label IDs that do not belong to the organization.
+func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) {
+ labels := make([]*Label, 0, len(labelIDs))
+ return labels, x.
+ Where("org_id = ?", orgID).
+ In("id", labelIDs).
+ Asc("name").
+ Find(&labels)
+}
+
+func getLabelsByOrgID(e Engine, orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
+ if orgID <= 0 {
+ return nil, ErrOrgLabelNotExist{0, orgID}
+ }
+ labels := make([]*Label, 0, 10)
+ sess := e.Where("org_id = ?", orgID)
+
+ switch sortType {
+ case "reversealphabetically":
+ sess.Desc("name")
+ case "leastissues":
+ sess.Asc("num_issues")
+ case "mostissues":
+ sess.Desc("num_issues")
+ default:
+ sess.Asc("name")
+ }
+
+ if listOptions.Page != 0 {
+ sess = listOptions.setSessionPagination(sess)
+ }
+
+ return labels, sess.Find(&labels)
+}
+
+// GetLabelsByOrgID returns all labels that belong to given organization by ID.
+func GetLabelsByOrgID(orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
+ return getLabelsByOrgID(x, orgID, sortType, listOptions)
+}
+
+// .___
+// | | ______ ________ __ ____
+// | |/ ___// ___/ | \_/ __ \
+// | |\___ \ \___ \| | /\ ___/
+// |___/____ >____ >____/ \___ |
+// \/ \/ \/
+
func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
var labels []*Label
return labels, e.Where("issue_label.issue_id = ?", issueID).
@@ -367,46 +605,6 @@ func updateLabel(e Engine, l *Label) error {
return err
}
-// UpdateLabel updates label information.
-func UpdateLabel(l *Label) error {
- if !LabelColorPattern.MatchString(l.Color) {
- return fmt.Errorf("bad color code: %s", l.Color)
- }
- return updateLabel(x, l)
-}
-
-// DeleteLabel delete a label of given repository.
-func DeleteLabel(repoID, labelID int64) error {
- _, err := GetLabelInRepoByID(repoID, labelID)
- if err != nil {
- if IsErrLabelNotExist(err) {
- return nil
- }
- return err
- }
-
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
- return err
- }
-
- if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
- return err
- } else if _, err = sess.
- Where("label_id = ?", labelID).
- Delete(new(IssueLabel)); err != nil {
- return err
- }
-
- // Clear label id in comment table
- if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil {
- return err
- }
-
- return sess.Commit()
-}
-
// .___ .____ ___. .__
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
diff --git a/models/issue_label_test.go b/models/issue_label_test.go
index 6f51473fcb..8afba779e0 100644
--- a/models/issue_label_test.go
+++ b/models/issue_label_test.go
@@ -66,10 +66,10 @@ func TestGetLabelInRepoByName(t *testing.T) {
assert.Equal(t, "label1", label.Name)
_, err = GetLabelInRepoByName(1, "")
- assert.True(t, IsErrLabelNotExist(err))
+ assert.True(t, IsErrRepoLabelNotExist(err))
_, err = GetLabelInRepoByName(NonexistentID, "nonexistent")
- assert.True(t, IsErrLabelNotExist(err))
+ assert.True(t, IsErrRepoLabelNotExist(err))
}
func TestGetLabelInRepoByNames(t *testing.T) {
@@ -103,10 +103,10 @@ func TestGetLabelInRepoByID(t *testing.T) {
assert.EqualValues(t, 1, label.ID)
_, err = GetLabelInRepoByID(1, -1)
- assert.True(t, IsErrLabelNotExist(err))
+ assert.True(t, IsErrRepoLabelNotExist(err))
_, err = GetLabelInRepoByID(NonexistentID, NonexistentID)
- assert.True(t, IsErrLabelNotExist(err))
+ assert.True(t, IsErrRepoLabelNotExist(err))
}
func TestGetLabelsInRepoByIDs(t *testing.T) {
@@ -135,6 +135,107 @@ func TestGetLabelsByRepoID(t *testing.T) {
testSuccess(1, "default", []int64{1, 2})
}
+// Org vrsions
+
+func TestGetLabelInOrgByName(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ label, err := GetLabelInOrgByName(3, "orglabel3")
+ assert.NoError(t, err)
+ assert.EqualValues(t, 3, label.ID)
+ assert.Equal(t, "orglabel3", label.Name)
+
+ _, err = GetLabelInOrgByName(3, "")
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByName(0, "orglabel3")
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByName(-1, "orglabel3")
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByName(NonexistentID, "nonexistent")
+ assert.True(t, IsErrOrgLabelNotExist(err))
+}
+
+func TestGetLabelInOrgByNames(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"})
+ assert.NoError(t, err)
+
+ assert.Len(t, labelIDs, 2)
+
+ assert.Equal(t, int64(3), labelIDs[0])
+ assert.Equal(t, int64(4), labelIDs[1])
+}
+
+func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ // orglabel99 doesn't exists.. See labels.yml
+ labelIDs, err := GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"})
+ assert.NoError(t, err)
+
+ assert.Len(t, labelIDs, 2)
+
+ assert.Equal(t, int64(3), labelIDs[0])
+ assert.Equal(t, int64(4), labelIDs[1])
+ assert.NoError(t, err)
+}
+
+func TestGetLabelInOrgByID(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ label, err := GetLabelInOrgByID(3, 3)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 3, label.ID)
+
+ _, err = GetLabelInOrgByID(3, -1)
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByID(0, 3)
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByID(-1, 3)
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelInOrgByID(NonexistentID, NonexistentID)
+ assert.True(t, IsErrOrgLabelNotExist(err))
+}
+
+func TestGetLabelsInOrgByIDs(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ labels, err := GetLabelsInOrgByIDs(3, []int64{3, 4, NonexistentID})
+ assert.NoError(t, err)
+ if assert.Len(t, labels, 2) {
+ assert.EqualValues(t, 3, labels[0].ID)
+ assert.EqualValues(t, 4, labels[1].ID)
+ }
+}
+
+func TestGetLabelsByOrgID(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
+ labels, err := GetLabelsByOrgID(orgID, sortType, ListOptions{})
+ assert.NoError(t, err)
+ assert.Len(t, labels, len(expectedIssueIDs))
+ for i, label := range labels {
+ assert.EqualValues(t, expectedIssueIDs[i], label.ID)
+ }
+ }
+ testSuccess(3, "leastissues", []int64{3, 4})
+ testSuccess(3, "mostissues", []int64{4, 3})
+ testSuccess(3, "reversealphabetically", []int64{4, 3})
+ testSuccess(3, "default", []int64{3, 4})
+
+ var err error
+ _, err = GetLabelsByOrgID(0, "leastissues", ListOptions{})
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+ _, err = GetLabelsByOrgID(-1, "leastissues", ListOptions{})
+ assert.True(t, IsErrOrgLabelNotExist(err))
+
+}
+
+//
+
func TestGetLabelsByIssueID(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
labels, err := GetLabelsByIssueID(1)
@@ -166,7 +267,7 @@ func TestDeleteLabel(t *testing.T) {
AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
assert.NoError(t, DeleteLabel(label.RepoID, label.ID))
- AssertNotExistsBean(t, &Label{ID: label.ID, RepoID: label.RepoID})
+ AssertNotExistsBean(t, &Label{ID: label.ID})
assert.NoError(t, DeleteLabel(NonexistentID, NonexistentID))
CheckConsistencyFor(t, &Label{}, &Repository{})
diff --git a/models/issue_list_test.go b/models/issue_list_test.go
index f5a91702f2..c9c39332c7 100644
--- a/models/issue_list_test.go
+++ b/models/issue_list_test.go
@@ -34,7 +34,6 @@ func TestIssueList_LoadAttributes(t *testing.T) {
setting.Service.EnableTimetracking = true
issueList := IssueList{
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
- AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 4}).(*Issue),
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 49b34861d6..847cd75d52 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -202,6 +202,8 @@ var migrations = []Migration{
NewMigration("Add EmailHash Table", addEmailHashTable),
// v134 -> v135
NewMigration("Refix merge base for merged pull requests", refixMergeBase),
+ // v135 -> 136
+ NewMigration("Add OrgID column to Labels table", addOrgIDLabelColumn),
}
// Migrate database to current version
diff --git a/models/migrations/v135.go b/models/migrations/v135.go
new file mode 100644
index 0000000000..8d859d42c0
--- /dev/null
+++ b/models/migrations/v135.go
@@ -0,0 +1,22 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func addOrgIDLabelColumn(x *xorm.Engine) error {
+ type Label struct {
+ OrgID int64 `xorm:"INDEX"`
+ }
+
+ if err := x.Sync2(new(Label)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ return nil
+}
diff --git a/modules/repository/create.go b/modules/repository/create.go
index 255bf09731..5c0aae30da 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -58,7 +58,7 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m
// Initialize Issue Labels if selected
if len(opts.IssueLabels) > 0 {
- if err = models.InitializeLabels(ctx, repo.ID, opts.IssueLabels); err != nil {
+ if err = models.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
return fmt.Errorf("InitializeLabels: %v", err)
}
}
diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go
index f9f0ec5d42..af47369ee1 100644
--- a/modules/test/context_tests.go
+++ b/modules/test/context_tests.go
@@ -45,8 +45,10 @@ func MockContext(t *testing.T, path string) *context.Context {
func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) {
ctx.Repo = &context.Repository{}
ctx.Repo.Repository = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repoID}).(*models.Repository)
- ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
var err error
+ ctx.Repo.Owner, err = models.GetUserByID(ctx.Repo.Repository.OwnerID)
+ assert.NoError(t, err)
+ ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
ctx.Repo.Permission, err = models.GetUserRepoPermission(ctx.Repo.Repository, ctx.User)
assert.NoError(t, err)
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2fb2e21a5c..ed6d74d35a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -725,6 +725,9 @@ tags = Tags
issues = Issues
pulls = Pull Requests
labels = Labels
+org_labels_desc = Organization level labels that can be used with <strong>all repositories</strong> under this organization
+org_labels_desc_manage = manage
+
milestones = Milestones
commits = Commits
commit = Commit
@@ -1699,6 +1702,8 @@ settings.delete_org_title = Delete Organization
settings.delete_org_desc = This organization will be deleted permanently. Continue?
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
+settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.
+
members.membership_visibility = Membership Visibility:
members.public = Visible
members.public_helper = make hidden
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index eee9440574..e5bb98033b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -861,6 +861,13 @@ func RegisterRoutes(m *macaron.Macaron) {
Post(reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
m.Get("/search", org.SearchTeam)
}, reqOrgMembership())
+ m.Group("/labels", func() {
+ m.Get("", org.ListLabels)
+ m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
+ m.Combo("/:id").Get(org.GetLabel).
+ Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
+ Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
+ })
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
Post(bind(api.CreateHookOption{}), org.CreateHook)
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
new file mode 100644
index 0000000000..c5fb262a30
--- /dev/null
+++ b/routers/api/v1/org/label.go
@@ -0,0 +1,237 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+)
+
+// ListLabels list all the labels of an organization
+func ListLabels(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/labels organization orgListLabels
+ // ---
+ // summary: List an organization's labels
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results, maximum page size is 50
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/LabelList"
+
+ labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), utils.GetListOptions(ctx))
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToLabelList(labels))
+}
+
+// CreateLabel create a label for a repository
+func CreateLabel(ctx *context.APIContext, form api.CreateLabelOption) {
+ // swagger:operation POST /orgs/{org}/labels organization orgCreateLabel
+ // ---
+ // summary: Create a label for an organization
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateLabelOption"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Label"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form.Color = strings.Trim(form.Color, " ")
+ if len(form.Color) == 6 {
+ form.Color = "#" + form.Color
+ }
+ if !models.LabelColorPattern.MatchString(form.Color) {
+ ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
+ return
+ }
+
+ label := &models.Label{
+ Name: form.Name,
+ Color: form.Color,
+ OrgID: ctx.Org.Organization.ID,
+ Description: form.Description,
+ }
+ if err := models.NewLabel(label); err != nil {
+ ctx.Error(http.StatusInternalServerError, "NewLabel", err)
+ return
+ }
+ ctx.JSON(http.StatusCreated, convert.ToLabel(label))
+}
+
+// GetLabel get label by organization and label id
+func GetLabel(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/labels/{id} organization orgGetLabel
+ // ---
+ // summary: Get a single label
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the label to get
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Label"
+
+ var (
+ label *models.Label
+ err error
+ )
+ strID := ctx.Params(":id")
+ if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
+ label, err = models.GetLabelInOrgByName(ctx.Org.Organization.ID, strID)
+ } else {
+ label, err = models.GetLabelInOrgByID(ctx.Org.Organization.ID, intID)
+ }
+ if err != nil {
+ if models.IsErrOrgLabelNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToLabel(label))
+}
+
+// EditLabel modify a label for an Organization
+func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
+ // swagger:operation PATCH /orgs/{org}/labels/{id} organization orgEditLabel
+ // ---
+ // summary: Update a label
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the label to edit
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditLabelOption"
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Label"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ label, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+ if err != nil {
+ if models.IsErrOrgLabelNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
+ }
+ return
+ }
+
+ if form.Name != nil {
+ label.Name = *form.Name
+ }
+ if form.Color != nil {
+ label.Color = strings.Trim(*form.Color, " ")
+ if len(label.Color) == 6 {
+ label.Color = "#" + label.Color
+ }
+ if !models.LabelColorPattern.MatchString(label.Color) {
+ ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
+ return
+ }
+ }
+ if form.Description != nil {
+ label.Description = *form.Description
+ }
+ if err := models.UpdateLabel(label); err != nil {
+ ctx.ServerError("UpdateLabel", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToLabel(label))
+}
+
+// DeleteLabel delete a label for an organization
+func DeleteLabel(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/labels/{id} organization orgDeleteLabel
+ // ---
+ // summary: Delete a label
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the label to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+
+ if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 25664e45a9..217c97c69b 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -81,8 +81,10 @@ func SearchIssues(ctx *context.APIContext) {
AllPublic: true,
TopicOnly: false,
Collaborate: util.OptionalBoolNone,
- OrderBy: models.SearchOrderByRecentUpdated,
- Actor: ctx.User,
+ // This needs to be a column that is not nil in fixtures or
+ // MySQL will return different results when sorting by null in some cases
+ OrderBy: models.SearchOrderByAlphabetically,
+ Actor: ctx.User,
}
if ctx.IsSigned {
opts.Private = true
@@ -152,6 +154,7 @@ func SearchIssues(ctx *context.APIContext) {
Page: ctx.QueryInt("page"),
PageSize: setting.UI.IssuePagingNum,
},
+
RepoIDs: repoIDs,
IsClosed: isClosed,
IssueIDs: issueIDs,
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index 8089891265..8b2a1988fa 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -171,12 +171,12 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
- label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+ label, err := models.GetLabelByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrLabelNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
- ctx.Error(http.StatusInternalServerError, "GetLabelInRepoByID", err)
+ ctx.Error(http.StatusInternalServerError, "GetLabelByID", err)
}
return
}
@@ -308,9 +308,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return
}
- labels, err = models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+ labels, err = models.GetLabelsByIDs(form.Labels)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
+ ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err)
return
}
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 95dbbc9551..5f70e74407 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -96,7 +96,7 @@ func GetLabel(ctx *context.APIContext) {
label, err = models.GetLabelInRepoByID(ctx.Repo.Repository.ID, intID)
}
if err != nil {
- if models.IsErrLabelNotExist(err) {
+ if models.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -197,7 +197,7 @@ func EditLabel(ctx *context.APIContext, form api.EditLabelOption) {
label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrLabelNotExist(err) {
+ if models.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
diff --git a/routers/org/org_labels.go b/routers/org/org_labels.go
new file mode 100644
index 0000000000..e5b9d9ddee
--- /dev/null
+++ b/routers/org/org_labels.go
@@ -0,0 +1,106 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package org
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/auth"
+ "code.gitea.io/gitea/modules/context"
+)
+
+// RetrieveLabels find all the labels of an organization
+func RetrieveLabels(ctx *context.Context) {
+ labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("RetrieveLabels.GetLabels", err)
+ return
+ }
+ for _, l := range labels {
+ l.CalOpenIssues()
+ }
+ ctx.Data["Labels"] = labels
+ ctx.Data["NumLabels"] = len(labels)
+ ctx.Data["SortType"] = ctx.Query("sort")
+}
+
+// NewLabel create new label for organization
+func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
+ ctx.Data["Title"] = ctx.Tr("repo.labels")
+ ctx.Data["PageIsLabels"] = true
+
+ if ctx.HasError() {
+ ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ return
+ }
+
+ l := &models.Label{
+ OrgID: ctx.Org.Organization.ID,
+ Name: form.Title,
+ Description: form.Description,
+ Color: form.Color,
+ }
+ if err := models.NewLabel(l); err != nil {
+ ctx.ServerError("NewLabel", err)
+ return
+ }
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
+
+// UpdateLabel update a label's name and color
+func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
+ l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
+ if err != nil {
+ switch {
+ case models.IsErrOrgLabelNotExist(err):
+ ctx.Error(404)
+ default:
+ ctx.ServerError("UpdateLabel", err)
+ }
+ return
+ }
+
+ l.Name = form.Title
+ l.Description = form.Description
+ l.Color = form.Color
+ if err := models.UpdateLabel(l); err != nil {
+ ctx.ServerError("UpdateLabel", err)
+ return
+ }
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
+
+// DeleteLabel delete a label
+func DeleteLabel(ctx *context.Context) {
+ if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.QueryInt64("id")); err != nil {
+ ctx.Flash.Error("DeleteLabel: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": ctx.Org.OrgLink + "/settings/labels",
+ })
+}
+
+// InitializeLabels init labels for an organization
+func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
+ if ctx.HasError() {
+ ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ return
+ }
+
+ if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Org.Organization.ID, form.TemplateName, true); err != nil {
+ if models.IsErrIssueLabelTemplateLoad(err) {
+ originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
+ ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ return
+ }
+ ctx.ServerError("InitializeLabels", err)
+ return
+ }
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+}
diff --git a/routers/org/setting.go b/routers/org/setting.go
index 3b6e124587..348d8cc8d8 100644
--- a/routers/org/setting.go
+++ b/routers/org/setting.go
@@ -24,6 +24,8 @@ const (
tplSettingsDelete base.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks base.TplName = "org/settings/hooks"
+ // tplSettingsLabels template path for render labels settings
+ tplSettingsLabels base.TplName = "org/settings/labels"
)
// Settings render the main settings page
@@ -177,3 +179,13 @@ func DeleteWebhook(ctx *context.Context) {
"redirect": ctx.Org.OrgLink + "/settings/hooks",
})
}
+
+// Labels render organization labels page
+func Labels(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.labels")
+ ctx.Data["PageIsOrgSettingsLabels"] = true
+ ctx.Data["RequireMinicolors"] = true
+ ctx.Data["RequireTribute"] = true
+ ctx.Data["LabelTemplates"] = models.LabelTemplates
+ ctx.HTML(200, tplSettingsLabels)
+}
diff --git a/routers/repo/compare.go b/routers/repo/compare.go
index 815ec35650..87b66dc7fb 100644
--- a/routers/repo/compare.go
+++ b/routers/repo/compare.go
@@ -335,7 +335,6 @@ func PrepareCompareDiff(
} else {
title = headBranch
}
-
ctx.Data["title"] = title
ctx.Data["Username"] = headUser.Name
ctx.Data["Reponame"] = headRepo.Name
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 9cc6ea1dfa..6dbf9cf5c8 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -259,6 +259,18 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
ctx.ServerError("GetLabelsByRepoID", err)
return
}
+
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+
+ ctx.Data["OrgLabels"] = orgLabels
+ labels = append(labels, orgLabels...)
+ }
+
for _, l := range labels {
l.LoadSelectedLabelsAfterClick(labelIDs)
}
@@ -377,6 +389,15 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull boo
return nil
}
ctx.Data["Labels"] = labels
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ return nil
+ }
+
+ ctx.Data["OrgLabels"] = orgLabels
+ labels = append(labels, orgLabels...)
+ }
RetrieveRepoMilestonesAndAssignees(ctx, repo)
if ctx.Written() {
@@ -593,6 +614,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
Content: form.Content,
Ref: form.Ref,
}
+
if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
if models.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(400, "UserDoesNotHaveAccessToRepo", err.Error())
@@ -761,6 +783,19 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("GetLabelsByRepoID", err)
return
}
+ ctx.Data["Labels"] = labels
+
+ if repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+ ctx.Data["OrgLabels"] = orgLabels
+
+ labels = append(labels, orgLabels...)
+ }
+
hasSelected := false
for i := range labels {
if labelIDMark[labels[i].ID] {
@@ -769,7 +804,6 @@ func ViewIssue(ctx *context.Context) {
}
}
ctx.Data["HasSelectedLabel"] = hasSelected
- ctx.Data["Labels"] = labels
// Check milestone and assignee.
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
diff --git a/routers/repo/issue_label.go b/routers/repo/issue_label.go
index 8ac9b8d336..16638404e3 100644
--- a/routers/repo/issue_label.go
+++ b/routers/repo/issue_label.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -35,7 +36,7 @@ func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
return
}
- if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName); err != nil {
+ if err := models.InitializeLabels(models.DefaultDBContext(), ctx.Repo.Repository.ID, form.TemplateName, false); err != nil {
if models.IsErrIssueLabelTemplateLoad(err) {
originalErr := err.(models.ErrIssueLabelTemplateLoad).OriginalError
ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr))
@@ -48,17 +49,47 @@ func InitializeLabels(ctx *context.Context, form auth.InitializeLabelsForm) {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
-// RetrieveLabels find all the labels of a repository
+// RetrieveLabels find all the labels of a repository and organization
func RetrieveLabels(ctx *context.Context) {
labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.Query("sort"), models.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
}
+
for _, l := range labels {
l.CalOpenIssues()
}
+
ctx.Data["Labels"] = labels
+
+ if ctx.Repo.Owner.IsOrganization() {
+ orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.Query("sort"), models.ListOptions{})
+ if err != nil {
+ ctx.ServerError("GetLabelsByOrgID", err)
+ return
+ }
+ for _, l := range orgLabels {
+ l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
+ }
+ ctx.Data["OrgLabels"] = orgLabels
+
+ org, err := models.GetOrgByName(ctx.Repo.Owner.LowerName)
+ if err != nil {
+ ctx.ServerError("GetOrgByName", err)
+ return
+ }
+ if ctx.User != nil {
+ ctx.Org.IsOwner, err = org.IsOwnedBy(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("org.IsOwnedBy", err)
+ return
+ }
+ ctx.Org.OrgLink = setting.AppSubURL + "/org/" + org.LowerName
+ ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
+ ctx.Data["OrganizationLink"] = ctx.Org.OrgLink
+ }
+ }
ctx.Data["NumLabels"] = len(labels)
ctx.Data["SortType"] = ctx.Query("sort")
}
@@ -89,10 +120,10 @@ func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
- l, err := models.GetLabelByID(form.ID)
+ l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
if err != nil {
switch {
- case models.IsErrLabelNotExist(err):
+ case models.IsErrRepoLabelNotExist(err):
ctx.Error(404)
default:
ctx.ServerError("UpdateLabel", err)
@@ -141,7 +172,7 @@ func UpdateIssueLabel(ctx *context.Context) {
case "attach", "detach", "toggle":
label, err := models.GetLabelByID(ctx.QueryInt64("id"))
if err != nil {
- if models.IsErrLabelNotExist(err) {
+ if models.IsErrRepoLabelNotExist(err) {
ctx.Error(404, "GetLabelByID")
} else {
ctx.ServerError("GetLabelByID", err)
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index e92f8a60b6..4409830dfe 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -594,6 +594,14 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/feishu/:id", bindIgnErr(auth.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
})
+ m.Group("/labels", func() {
+ m.Get("", org.RetrieveLabels, org.Labels)
+ m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), org.NewLabel)
+ m.Post("/edit", bindIgnErr(auth.CreateLabelForm{}), org.UpdateLabel)
+ m.Post("/delete", org.DeleteLabel)
+ m.Post("/initialize", bindIgnErr(auth.InitializeLabelsForm{}), org.InitializeLabels)
+ })
+
m.Route("/delete", "GET,POST", org.SettingsDelete)
})
}, context.OrgAssignment(true, true))
diff --git a/services/issue/label.go b/services/issue/label.go
index d2c1cd6ec5..c8ef9e9536 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -51,7 +51,10 @@ func RemoveLabel(issue *models.Issue, doer *models.User, label *models.Label) er
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
- return models.ErrLabelNotExist{}
+ if label.OrgID > 0 {
+ return models.ErrOrgLabelNotExist{}
+ }
+ return models.ErrRepoLabelNotExist{}
}
if err := models.DeleteIssueLabel(issue, label, doer); err != nil {
diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl
new file mode 100644
index 0000000000..ed31890df5
--- /dev/null
+++ b/templates/org/settings/labels.tmpl
@@ -0,0 +1,29 @@
+{{template "base/head" .}}
+<div class="organization settings labels">
+ {{template "org/header" .}}
+ <div class="ui container">
+ <div class="ui grid">
+ {{template "org/settings/navbar" .}}
+ <div class="ui twelve wide column content">
+ <div class="ui grid">
+ <div class="left floated twelve wide column">
+ {{$.i18n.Tr "org.settings.labels_desc" | Str2html}}
+ </div>
+ <div class="right floated three wide column">
+ <div class="ui right">
+ <div class="ui green new-label button">{{.i18n.Tr "repo.issues.new_label"}}</div>
+ </div>
+ </div>
+ </div>
+ <div class="ui divider"></div>
+ {{template "repo/issue/labels/label_new" .}}
+ {{template "base/alert" .}}
+ {{template "repo/issue/labels/label_list" .}}
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+{{template "repo/issue/labels/edit_delete_label" .}}
+{{template "base/footer" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 09fca5d7f6..63114b056e 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -7,6 +7,9 @@
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.OrgLink}}/settings/hooks">
{{.i18n.Tr "repo.settings.hooks"}}
</a>
+ <a class="{{if .PageIsOrgSettingsLabels}}active{{end}} item" href="{{.OrgLink}}/settings/labels">
+ {{.i18n.Tr "repo.labels"}}
+ </a>
<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{.OrgLink}}/settings/delete">
{{.i18n.Tr "org.settings.delete"}}
</a>
diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl
index 4719c8f1fb..d3df3b5944 100644
--- a/templates/repo/issue/labels.tmpl
+++ b/templates/repo/issue/labels.tmpl
@@ -10,173 +10,17 @@
</div>
{{end}}
</div>
- {{if not .Repository.IsArchived}}
- <div class="ui new-label segment hide">
- <form class="ui form" action="{{$.RepoLink}}/labels/new" method="post">
- {{.CsrfTokenHtml}}
- <div class="ui grid">
- <div class="three wide column">
- <div class="ui small input">
- <input class="new-label-input emoji-input" name="title" placeholder="{{.i18n.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
- </div>
- </div>
- <div class="five wide column">
- <div class="ui small fluid input">
- <input class="new-label-desc-input" name="description" placeholder="{{.i18n.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
- </div>
- </div>
- <div class="color picker column">
- <input class="color-picker" name="color" value="#70c24a" required maxlength="7">
- </div>
- <div class="column precolors">
- {{template "repo/issue/label_precolors"}}
- </div>
- <div class="buttons">
- <div class="ui blue small basic cancel button">{{.i18n.Tr "repo.milestones.cancel"}}</div>
- <button class="ui green small button">{{.i18n.Tr "repo.issues.create_label"}}</button>
- </div>
- </div>
- </form>
- </div>
- {{end}}
<div class="ui divider"></div>
-
- <div class="ui right floated secondary filter menu">
- <!-- Sort -->
- <div class="ui dropdown type jump item">
- <span class="text">
- {{.i18n.Tr "repo.issues.filter_sort"}}
- <i class="dropdown icon"></i>
- </span>
- <div class="menu">
- <a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=alphabetically&state={{$.State}}">{{.i18n.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
- <a class="{{if eq .SortType "reversealphabetically"}}active{{end}} item" href="{{$.Link}}?sort=reversealphabetically&state={{$.State}}">{{.i18n.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
- <a class="{{if eq .SortType "leastissues"}}active{{end}} item" href="{{$.Link}}?sort=leastissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_issues"}}</a>
- <a class="{{if eq .SortType "mostissues"}}active{{end}} item" href="{{$.Link}}?sort=mostissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_issues"}}</a>
- </div>
- </div>
- </div>
+ {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
+ {{template "repo/issue/labels/label_new" .}}
+ {{end}}
{{template "base/alert" .}}
- <div class="ui black basic label">{{.i18n.Tr "repo.issues.label_count" .NumLabels}}</div>
- <div class="label list">
- {{if and (or $.CanWriteIssues $.CanWritePulls) (eq .NumLabels 0) (not $.Repository.IsArchived) }}
- <div class="ui centered grid">
- <div class="twelve wide column eight wide computer column">
- <div class="ui attached left aligned segment">
- <!-- <h4 class="ui header">
- {{.i18n.Tr "repo.issues.label_templates.title"}}
- <a target="_blank" rel="noopener noreferrer"
- href="https://discuss.gogs.io/t/how-to-use-predefined-label-templates/599">
- <span class="octicon octicon-question"></span>
- </a>
- </h4> -->
- <p>{{.i18n.Tr "repo.issues.label_templates.info"}}</p>
- <br/>
- <form class="ui form center" action="{{.Link}}/initialize" method="post">
- {{.CsrfTokenHtml}}
- <div class="field">
- <div class="ui selection dropdown">
- <input type="hidden" name="template_name" value="Default">
- <div class="default text">{{.i18n.Tr "repo.issues.label_templates.helper"}}</div>
- <div class="menu">
- {{range $template, $labels := .LabelTemplates}}
- <div class="item" data-value="{{$template}}">{{$template}}<br/><i>({{$labels}})</i></div>
- {{end}}
- </div>
- </div>
- </div>
- <button type="submit" class="ui blue button">{{.i18n.Tr "repo.issues.label_templates.use"}}</button>
- </form>
- </div>
- </div>
- </div>
- {{end}}
-
- <div class="ui divider"></div>
-
- {{range .Labels}}
- <li class="item">
- <div class="ui grid">
- <div class="three wide column">
- <div class="ui label has-emoji" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{svg "octicon-tag" 16}} {{.Name}}</div>
- </div>
- <div class="seven wide column">
- {{.Description}}
- </div>
- <div class="three wide column">
- <a class="ui right open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a>
- </div>
- <div class="three wide column">
- {{if and (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
- <a class="ui right delete-button" href="#" data-url="{{$.RepoLink}}/labels/delete" data-id="{{.ID}}">{{svg "octicon-trashcan" 16}} {{$.i18n.Tr "repo.issues.label_delete"}}</a>
- <a class="ui right edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil" 16}} {{$.i18n.Tr "repo.issues.label_edit"}}</a>
- {{end}}
- </div>
- </div>
- </li>
- {{end}}
- </div>
+ {{template "repo/issue/labels/label_list" .}}
</div>
</div>
-{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
- <div class="ui small basic delete modal">
- <div class="ui icon header">
- <i class="trash icon"></i>
- {{.i18n.Tr "repo.issues.label_deletion"}}
- </div>
- <div class="content">
- <p>{{.i18n.Tr "repo.issues.label_deletion_desc"}}</p>
- </div>
- <div class="actions">
- <div class="ui red basic inverted cancel button">
- <i class="remove icon"></i>
- {{.i18n.Tr "modal.no"}}
- </div>
- <div class="ui green basic inverted ok button">
- <i class="checkmark icon"></i>
- {{.i18n.Tr "modal.yes"}}
- </div>
- </div>
- </div>
-
- <div class="ui small edit-label modal">
- <div class="header">
- {{.i18n.Tr "repo.issues.label_modify"}}
- </div>
- <div class="content">
- <form class="ui edit-label form" action="{{$.RepoLink}}/labels/edit" method="post">
- {{.CsrfTokenHtml}}
- <input id="label-modal-id" name="id" type="hidden">
- <div class="ui grid">
- <div class="three wide column">
- <div class="ui small input">
- <input class="new-label-input emoji-input" name="title" placeholder="{{.i18n.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
- </div>
- </div>
- <div class="five wide column">
- <div class="ui small fluid input">
- <input class="new-label-desc-input" name="description" placeholder="{{.i18n.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
- </div>
- </div>
- <div class="color picker column">
- <input class="color-picker" name="color" value="#70c24a" required maxlength="7">
- </div>
- <div class="column precolors">
- {{template "repo/issue/label_precolors"}}
- </div>
- </div>
- </form>
- </div>
- <div class="actions">
- <div class="ui negative button">
- {{.i18n.Tr "modal.no"}}
- </div>
- <div class="ui positive right labeled icon button">
- {{.i18n.Tr "modal.modify"}}
- <i class="checkmark icon"></i>
- </div>
- </div>
- </div>
+{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived) }}
+{{template "repo/issue/labels/edit_delete_label" .}}
{{end}}
+</div>
{{template "base/footer" .}}
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
new file mode 100644
index 0000000000..1ddfd39ee4
--- /dev/null
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -0,0 +1,59 @@
+<div class="ui small basic delete modal">
+ <div class="ui icon header">
+ <i class="trash icon"></i>
+ {{.i18n.Tr "repo.issues.label_deletion"}}
+ </div>
+ <div class="content">
+ <p>{{.i18n.Tr "repo.issues.label_deletion_desc"}}</p>
+ </div>
+ <div class="actions">
+ <div class="ui red basic inverted cancel button">
+ <i class="remove icon"></i>
+ {{.i18n.Tr "modal.no"}}
+ </div>
+ <div class="ui green basic inverted ok button">
+ <i class="checkmark icon"></i>
+ {{.i18n.Tr "modal.yes"}}
+ </div>
+ </div>
+ </div>
+
+ <div class="ui small edit-label modal">
+ <div class="header">
+ {{.i18n.Tr "repo.issues.label_modify"}}
+ </div>
+ <div class="content">
+ <form class="ui edit-label form" action="{{$.Link}}/edit" method="post">
+ {{.CsrfTokenHtml}}
+ <input id="label-modal-id" name="id" type="hidden">
+ <div class="ui grid">
+ <div class="three wide column">
+ <div class="ui small input">
+ <input class="new-label-input emoji-input" name="title" placeholder="{{.i18n.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
+ </div>
+ </div>
+ <div class="five wide column">
+ <div class="ui small fluid input">
+ <input class="new-label-desc-input" name="description" placeholder="{{.i18n.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
+ </div>
+ </div>
+ <div class="color picker column">
+ <input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+ </div>
+ <div class="column precolors">
+ {{template "repo/issue/label_precolors"}}
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="actions">
+ <div class="ui negative button">
+ {{.i18n.Tr "modal.no"}}
+ </div>
+ <div class="ui positive right labeled icon button">
+ {{.i18n.Tr "modal.modify"}}
+ <i class="checkmark icon"></i>
+ </div>
+ </div>
+ </div>
+
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
new file mode 100644
index 0000000000..ce33d76fc1
--- /dev/null
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -0,0 +1,97 @@
+<h4 class="ui top attached header">
+ {{.i18n.Tr "repo.issues.label_count" .NumLabels}}
+ <div class="ui right">
+ <div class="ui right floated secondary filter menu">
+ <!-- Sort -->
+ <div class="ui dropdown type jump item">
+ <span class="text">
+ {{.i18n.Tr "repo.issues.filter_sort"}}
+ <i class="dropdown icon"></i>
+ </span>
+ <div class="menu">
+ <a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=alphabetically&state={{$.State}}">{{.i18n.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+ <a class="{{if eq .SortType "reversealphabetically"}}active{{end}} item" href="{{$.Link}}?sort=reversealphabetically&state={{$.State}}">{{.i18n.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
+ <a class="{{if eq .SortType "leastissues"}}active{{end}} item" href="{{$.Link}}?sort=leastissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_issues"}}</a>
+ <a class="{{if eq .SortType "mostissues"}}active{{end}} item" href="{{$.Link}}?sort=mostissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_issues"}}</a>
+ </div>
+ </div>
+ </div>
+ </div> <!-- filter menu -->
+</h4>
+
+<div class="ui attached segment">
+ <div class="labelspage">
+ {{if and (not $.PageIsOrgSettingsLabels) (or $.CanWriteIssues $.CanWritePulls) (eq .NumLabels 0) (not $.Repository.IsArchived) }}
+ {{template "repo/issue/labels/label_load_template" .}}
+ <div class="ui divider"></div>
+ {{else if and ($.PageIsOrgSettingsLabels) (eq .NumLabels 0)}}
+ {{template "repo/issue/labels/label_load_template" .}}
+ {{end}}
+ {{range .Labels}}
+ <li class="item">
+ <div class="ui grid middle aligned">
+ <div class="four wide column">
+ <div class="ui label has-emoji" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{svg "octicon-tag" 16}} {{.Name}}</div>
+ </div>
+ <div class="six wide column">
+ <div class="ui has-emoji">
+ {{.Description}}
+ </div>
+ </div>
+ <div class="three wide column">
+ {{if $.PageIsOrgSettingsLabels}}
+ <a class="ui right open-issues" href="/issues?labels={{.ID}}">{{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a>
+ {{else}}
+ <a class="ui right open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a>
+ {{end}}
+ </div>
+ <div class="three wide column">
+ {{if and (not $.PageIsOrgSettingsLabels ) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
+ <a class="ui right delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trashcan" 16}} {{$.i18n.Tr "repo.issues.label_delete"}}</a>
+ <a class="ui right edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil" 16}} {{$.i18n.Tr "repo.issues.label_edit"}}</a>
+ {{else if $.PageIsOrgSettingsLabels}}
+ <a class="ui right delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trashcan" 16}} {{$.i18n.Tr "repo.issues.label_delete"}}</a>
+ <a class="ui right edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil" 16}} {{$.i18n.Tr "repo.issues.label_edit"}}</a>
+ {{end}}
+ </div>
+ </div>
+ </li>
+ {{end}}
+ {{if and (not .PageIsOrgSettingsLabels) (.OrgLabels) }}
+ <li class="item">
+ <div class="ui grid middle aligned">
+ <div class="ten wide column">
+ {{$.i18n.Tr "repo.org_labels_desc" | Str2html}}
+ {{if .IsOrganizationOwner}}
+ <a class="ui" href="{{.OrganizationLink}}/settings/labels">({{$.i18n.Tr "repo.org_labels_desc_manage"}})</a>:
+ {{end}}
+ </div>
+ </div>
+ </li>
+ {{if (not $.PageIsOrgSettingsLabels)}}
+ <div class="orglabel">
+ {{range .OrgLabels}}
+ <li class="item">
+ <div class="ui grid middle aligned">
+ <div class="three wide column">
+ <div class="ui label has-emoji" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{svg "octicon-tag" 16}} {{.Name}}</div>
+ </div>
+ <div class="seven wide column">
+ <div class="ui has-emoji">
+ {{.Description}}
+ </div>
+ </div>
+ <div class="three wide column">
+ <a class="ui right open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.label_open_issues" .NumOpenRepoIssues}}</a>
+ </div>
+ <div class="three wide column">
+ </div>
+ </div>
+ </li>
+ {{end}}
+ </div>
+ {{end}}
+ {{end}}
+ </div>
+</div>
+
diff --git a/templates/repo/issue/labels/label_load_template.tmpl b/templates/repo/issue/labels/label_load_template.tmpl
new file mode 100644
index 0000000000..76ee77658a
--- /dev/null
+++ b/templates/repo/issue/labels/label_load_template.tmpl
@@ -0,0 +1,30 @@
+<div class="ui centered grid">
+ <div class="twelve wide computer column">
+ <div class="ui attached left aligned segment">
+ <!-- <h4 class="ui header">
+ {{.i18n.Tr "repo.issues.label_templates.title"}}
+ <a target="_blank" rel="noopener noreferrer"
+ href="https://discuss.gogs.io/t/how-to-use-predefined-label-templates/599">
+ <span class="octicon octicon-question"></span>
+ </a>
+ </h4> -->
+ <p>{{.i18n.Tr "repo.issues.label_templates.info"}}</p>
+ <br/>
+ <form class="ui form center" action="{{.Link}}/initialize" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field">
+ <div class="ui selection dropdown">
+ <input type="hidden" name="template_name" value="Default">
+ <div class="default text">{{.i18n.Tr "repo.issues.label_templates.helper"}}</div>
+ <div class="menu">
+ {{range $template, $labels := .LabelTemplates}}
+ <div class="item" data-value="{{$template}}">{{$template}}<br/><i>({{$labels}})</i></div>
+ {{end}}
+ </div>
+ </div>
+ </div>
+ <button type="submit" class="ui blue button">{{.i18n.Tr "repo.issues.label_templates.use"}}</button>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
new file mode 100644
index 0000000000..15a6c029a3
--- /dev/null
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -0,0 +1,27 @@
+<div class="ui new-label segment hide">
+ <form class="ui form" action="{{$.Link}}/new" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="ui grid">
+ <div class="three wide column">
+ <div class="ui small input">
+ <input class="new-label-input emoji-input" name="title" placeholder="{{.i18n.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
+ </div>
+ </div>
+ <div class="three wide column">
+ <div class="ui small fluid input">
+ <input class="new-label-desc-input" name="description" placeholder="{{.i18n.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
+ </div>
+ </div>
+ <div class="color picker column">
+ <input class="color-picker" name="color" value="#70c24a" required maxlength="7">
+ </div>
+ <div class="column precolors">
+ {{template "repo/issue/label_precolors"}}
+ </div>
+ <div class="buttons">
+ <div class="ui blue small basic cancel button">{{.i18n.Tr "repo.milestones.cancel"}}</div>
+ <button class="ui green small button">{{.i18n.Tr "repo.issues.create_label"}}</button>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 524b849c14..a53bbdc685 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -38,7 +38,7 @@
{{template "repo/issue/branch_selector_field" .}}
<input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
- <div class="ui {{if not .Labels}}disabled{{end}} floating jump select-label dropdown">
+ <div class="ui {{if and (not .Labels) (not .OrgLabels)}}disabled{{end}} floating jump select-label dropdown">
<span class="text">
<strong>{{.i18n.Tr "repo.issues.new.labels"}}</strong>
{{svg "octicon-gear" 16}}
@@ -49,6 +49,11 @@
<a class="{{if .IsChecked}}checked{{end}} item has-emoji" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon-check {{if not .IsChecked}}invisible{{end}}">{{svg "octicon-check" 16}}</span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}
{{if .Description }}<br><small class="desc">{{.Description}}</small>{{end}}</a>
{{end}}
+ <div class="ui divider"></div>
+ {{range .OrgLabels}}
+ <a class="{{if .IsChecked}}checked{{end}} item has-emoji" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon-check {{if not .IsChecked}}invisible{{end}}">{{svg "octicon-check" 16}}</span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}
+ {{if .Description }}<br><small class="desc">{{.Description}}</small>{{end}}</a>
+ {{end}}
</div>
</div>
<div class="ui labels list">
@@ -56,6 +61,9 @@
{{range .Labels}}
<a class="{{if not .IsChecked}}hide{{end}} item" id="label_{{.ID}}" href="{{$.RepoLink}}/issues?labels={{.ID}}"><span class="label color" style="background-color: {{.Color}}"></span> <span class="text has-emoji">{{.Name}}</span></a>
{{end}}
+ {{range .OrgLabels}}
+ <a class="{{if not .IsChecked}}hide{{end}} item" id="label_{{.ID}}" href="/issues?labels={{.ID}}"><span class="label color" style="background-color: {{.Color}}"></span> <span class="text has-emoji">{{.Name}}</span></a>
+ {{end}}
</div>
<div class="ui divider"></div>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 1d1f1916da..d0275c23f4 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -15,6 +15,11 @@
<a class="{{if .IsChecked}}checked{{end}} item has-emoji" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon-check {{if not .IsChecked}}invisible{{end}}">{{svg "octicon-check" 16}}</span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}
{{if .Description }}<br><small class="desc">{{.Description}}</small>{{end}}</a>
{{end}}
+ <div class="ui divider"></div>
+ {{range .OrgLabels}}
+ <a class="{{if .IsChecked}}checked{{end}} item has-emoji" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon-check {{if not .IsChecked}}invisible{{end}}">{{svg "octicon-check" 16}}</span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}
+ {{if .Description }}<br><small class="desc">{{.Description}}</small>{{end}}</a>
+ {{end}}
</div>
</div>
<div class="ui labels list">
@@ -23,6 +28,11 @@
<div class="item">
<a class="ui label has-emoji {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/issues?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description}}">{{.Name}}</a>
</div>
+ {{end}}
+ {{range .OrgLabels}}
+ <div class="item">
+ <a class="ui label has-emoji {{if not .IsChecked}}hide{{end}}" id="label_{{.ID}}" href="{{$.RepoLink}}/issues?labels={{.ID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}" title="{{.Description}}">{{.Name}}</a>
+ </div>
{{end}}
</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c564f8739b..6e5086d507 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -981,6 +981,189 @@
}
}
},
+ "/orgs/{org}/labels": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "List an organization's labels",
+ "operationId": "orgListLabels",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results, maximum page size is 50",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/LabelList"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Create a label for an organization",
+ "operationId": "orgCreateLabel",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/CreateLabelOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/Label"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
+ "/orgs/{org}/labels/{id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Get a single label",
+ "operationId": "orgGetLabel",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the label to get",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/Label"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "organization"
+ ],
+ "summary": "Delete a label",
+ "operationId": "orgDeleteLabel",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the label to delete",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ }
+ }
+ },
+ "patch": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Update a label",
+ "operationId": "orgEditLabel",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "id of the label to edit",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditLabelOption"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/Label"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/orgs/{org}/members": {
"get": {
"produces": [
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 2db5b08b8b..a0e410c133 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -125,6 +125,39 @@ function initBranchSelector() {
});
}
+function initLabelEdit() {
+// Create label
+ const $newLabelPanel = $('.new-label.segment');
+ $('.new-label.button').click(() => {
+ $newLabelPanel.show();
+ });
+ $('.new-label.segment .cancel').click(() => {
+ $newLabelPanel.hide();
+ });
+
+ $('.color-picker').each(function () {
+ $(this).minicolors();
+ });
+ $('.precolors .color').click(function () {
+ const color_hex = $(this).data('color-hex');
+ $('.color-picker').val(color_hex);
+ $('.minicolors-swatch-color').css('background-color', color_hex);
+ });
+ $('.edit-label-button').click(function () {
+ $('#label-modal-id').val($(this).data('id'));
+ $('.edit-label .new-label-input').val($(this).data('title'));
+ $('.edit-label .new-label-desc-input').val($(this).data('description'));
+ $('.edit-label .color-picker').val($(this).data('color'));
+ $('.minicolors-swatch-color').css('background-color', $(this).data('color'));
+ $('.edit-label.modal').modal({
+ onApprove() {
+ $('.edit-label.form').submit();
+ }
+ }).modal('show');
+ return false;
+ });
+}
+
function updateIssuesMeta(url, action, issueIds, elementId) {
return new Promise(((resolve) => {
$.ajax({
@@ -697,36 +730,7 @@ async function initRepository() {
// Labels
if ($('.repository.labels').length > 0) {
- // Create label
- const $newLabelPanel = $('.new-label.segment');
- $('.new-label.button').click(() => {
- $newLabelPanel.show();
- });
- $('.new-label.segment .cancel').click(() => {
- $newLabelPanel.hide();
- });
-
- $('.color-picker').each(function () {
- $(this).minicolors();
- });
- $('.precolors .color').click(function () {
- const color_hex = $(this).data('color-hex');
- $('.color-picker').val(color_hex);
- $('.minicolors-swatch-color').css('background-color', color_hex);
- });
- $('.edit-label-button').click(function () {
- $('#label-modal-id').val($(this).data('id'));
- $('.edit-label .new-label-input').val($(this).data('title'));
- $('.edit-label .new-label-desc-input').val($(this).data('description'));
- $('.edit-label .color-picker').val($(this).data('color'));
- $('.minicolors-swatch-color').css('background-color', $(this).data('color'));
- $('.edit-label.modal').modal({
- onApprove() {
- $('.edit-label.form').submit();
- }
- }).modal('show');
- return false;
- });
+ initLabelEdit();
}
// Milestones
@@ -1757,6 +1761,11 @@ function initOrganization() {
}
});
}
+
+ // Labels
+ if ($('.organization.settings.labels').length > 0) {
+ initLabelEdit();
+ }
}
function initUserSettings() {
diff --git a/web_src/less/_organization.less b/web_src/less/_organization.less
index 6071604cbc..5a72017c2f 100644
--- a/web_src/less/_organization.less
+++ b/web_src/less/_organization.less
@@ -168,4 +168,45 @@
height: 60px;
}
}
+
+ &.settings {
+ .labelspage {
+ list-style: none;
+ padding-top: 0;
+
+ .item {
+ margin-top: 0;
+ margin-right: -14px;
+ margin-left: -14px !important;
+ padding: 10px;
+ border-bottom: 1px solid #e1e4e8;
+ border-top: none;
+
+ a {
+ font-size: 15px;
+ padding-top: 5px;
+ padding-right: 10px;
+ color: #666666;
+
+ &:hover {
+ color: #000000;
+ }
+
+ &.open-issues {
+ margin-right: 30px;
+ }
+ }
+
+ .ui.label {
+ font-size: 1em;
+ }
+ }
+
+ .item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+
+ }
+ }
}
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index bc7344ba0b..3b2467b500 100644
--- a/web_src/less/_repository.less
+++ b/web_src/less/_repository.less
@@ -1045,14 +1045,17 @@
}
}
- .label.list {
+ .labelspage {
list-style: none;
- padding-top: 15px;
+ padding-top: 0;
.item {
- padding-top: 10px;
- padding-bottom: 10px;
- border-bottom: 1px dashed #aaaaaa;
+ margin-top: 0;
+ margin-right: -14px;
+ margin-left: -14px;
+ padding: 10px;
+ border-bottom: 1px solid #e1e4e8;
+ border-top: none;
a {
font-size: 15px;
@@ -1073,6 +1076,16 @@
font-size: 1em;
}
}
+
+ .item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+
+ .orglabel {
+ opacity: .7;
+ }
+
}
.milestone.list {