diff --git a/.golangci.yml b/.golangci.yml index 0e796a2016b0a..599f7f1bbdbc7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -177,3 +177,15 @@ issues: linters: - revive text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge" + - path: routers/web/repo/issue_priority.go + linters: + - dupl + - path: routers/web/repo/issue_label.go + linters: + - dupl + - path: models/issues/priority.go + linters: + - dupl + - path: models/issues/label.go + linters: + - dupl diff --git a/models/issues/comment.go b/models/issues/comment.go index a71afda9e0e8f..6b80688e7d73a 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -130,6 +130,8 @@ const ( CommentTypePRScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge + // 35 Priority changed + CommentTypePriority ) var commentStrings = []string{ @@ -225,6 +227,8 @@ type Comment struct { Label *Label `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` + PriorityID int64 + Priority *Priority `xorm:"-"` OldProjectID int64 ProjectID int64 OldProject *project_model.Project `xorm:"-"` @@ -960,11 +964,12 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is // CreateCommentOptions defines options for creating comment type CreateCommentOptions struct { - Type CommentType - Doer *user_model.User - Repo *repo_model.Repository - Issue *Issue - Label *Label + Type CommentType + Doer *user_model.User + Repo *repo_model.Repository + Issue *Issue + Label *Label + Priority *Priority DependentIssueID int64 OldMilestoneID int64 diff --git a/models/issues/issue.go b/models/issues/issue.go index 5bdb60f7c08c5..bbab2290c138d 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -117,7 +117,8 @@ type Issue struct { MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Project *project_model.Project `xorm:"-"` - Priority int + PriorityID int64 + Priority *Priority `xorm:"-"` AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` @@ -236,6 +237,17 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) { return nil } +// LoadPriorities loads labels +func (issue *Issue) LoadPriorities(ctx context.Context) (err error) { + if issue.Priority == nil { + issue.Priority, err = GetPriorityByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getPriorityByIssueID [%d]: %v", issue.ID, err) + } + } + return nil +} + // LoadPoster loads poster func (issue *Issue) LoadPoster() error { return issue.loadPoster(db.DefaultContext) @@ -1192,6 +1204,7 @@ type IssuesOptions struct { //nolint IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 + PriorityIDs []int64 IncludedLabelNames []string ExcludedLabelNames []string IncludeMilestones []string diff --git a/models/issues/priority.go b/models/issues/priority.go new file mode 100644 index 0000000000000..8ce132a07fa1c --- /dev/null +++ b/models/issues/priority.go @@ -0,0 +1,761 @@ +// Copyright 2022 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 issues + +import ( + "context" + "fmt" + "html/template" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// ErrRepoPriorityNotExist represents a "RepoPriorityNotExist" kind of error. +type ErrRepoPriorityNotExist struct { + PriorityID int64 + RepoID int64 +} + +// IsErrRepoPriorityNotExist checks if an error is a RepoErrPriorityNotExist. +func IsErrRepoPriorityNotExist(err error) bool { + _, ok := err.(ErrRepoPriorityNotExist) + return ok +} + +func (err ErrRepoPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, repo_id: %d]", err.PriorityID, err.RepoID) +} + +// ErrOrgPriorityNotExist represents a "OrgPriorityNotExist" kind of error. +type ErrOrgPriorityNotExist struct { + PriorityID int64 + OrgID int64 +} + +// IsErrOrgPriorityNotExist checks if an error is a OrgErrPriorityNotExist. +func IsErrOrgPriorityNotExist(err error) bool { + _, ok := err.(ErrOrgPriorityNotExist) + return ok +} + +func (err ErrOrgPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, org_id: %d]", err.PriorityID, err.OrgID) +} + +// ErrPriorityNotExist represents a "PriorityNotExist" kind of error. +type ErrPriorityNotExist struct { + PriorityID int64 +} + +// IsErrPriorityNotExist checks if an error is a ErrPriorityNotExist. +func IsErrPriorityNotExist(err error) bool { + _, ok := err.(ErrPriorityNotExist) + return ok +} + +func (err ErrPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d]", err.PriorityID) +} + +// PriorityColorPattern is a regexp witch can validate PriorityColor +var PriorityColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + +// Priority represents a priority of repository for issues. +type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + NumOpenIssues int `xorm:"-"` + NumOpenRepoIssues int64 `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Priority)) +} + +// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. +func (priority *Priority) CalOpenIssues() { + priority.NumOpenIssues = priority.NumIssues - priority.NumClosedIssues +} + +// CalOpenOrgIssues calculates the open issues of a label for a specific repo +func (priority *Priority) CalOpenOrgIssues(repoID, priorityID int64) { + counts, _ := CountIssuesByRepo(&IssuesOptions{ + RepoID: repoID, + PriorityIDs: []int64{priorityID}, + IsClosed: util.OptionalBoolFalse, + }) + + for _, count := range counts { + priority.NumOpenRepoIssues += count + } +} + +// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked +func (priority *Priority) LoadSelectedPrioritiesAfterClick(currentSelectedPriorities []int64) { + var priorityQuerySlice []string + labelSelected := false + labelID := strconv.FormatInt(priority.ID, 10) + for _, s := range currentSelectedPriorities { + if s == priority.ID { + labelSelected = true + } else if -s == priority.ID { + labelSelected = true + priority.IsExcluded = true + } else if s != 0 { + priorityQuerySlice = append(priorityQuerySlice, strconv.FormatInt(s, 10)) + } + } + if !labelSelected { + priorityQuerySlice = append(priorityQuerySlice, labelID) + } + priority.IsSelected = labelSelected + priority.QueryString = strings.Join(priorityQuerySlice, ",") +} + +// BelongsToOrg returns true if label is an organization label +func (priority *Priority) BelongsToOrg() bool { + return priority.OrgID > 0 +} + +// BelongsToRepo returns true if label is a repository label +func (priority *Priority) BelongsToRepo() bool { + return priority.RepoID > 0 +} + +// ForegroundColor calculates the text color for labels based +// on their background color. +func (priority *Priority) ForegroundColor() template.CSS { + if strings.HasPrefix(priority.Color, "#") { + if color, err := strconv.ParseUint(priority.Color[1:], 16, 64); err == nil { + // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation + luminance := Luminance(uint32(color)) + + // prefer white or black based upon contrast + if luminance < LuminanceThreshold { + return template.CSS("#fff") + } + return template.CSS("#000") + } + } + + // default to black + return template.CSS("#000") +} + +// NewPriority creates a new Priority +func NewPriority(ctx context.Context, priority *Priority) error { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + + // normalize case + priority.Color = strings.ToLower(priority.Color) + + // add leading hash + if priority.Color[0] != '#' { + priority.Color = "#" + priority.Color + } + + // convert 3-character shorthand into 6-character version + if len(priority.Color) == 4 { + r := priority.Color[1] + g := priority.Color[2] + b := priority.Color[3] + priority.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) + } + + return db.Insert(ctx, priority) +} + +// NewPriorities creates new labels +func NewPriorities(priorities ...*Priority) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + for _, priority := range priorities { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + if err := db.Insert(ctx, priority); err != nil { + return err + } + } + return committer.Commit() +} + +// UpdateLabel updates label information. +func UpdatePriority(priority *Priority) error { + if !PriorityColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + return updatePriorityCols(db.DefaultContext, priority, "name", "description", "color", "weight") +} + +// DeletePriority delete a label +func DeletePriority(id, priorityID int64) error { + priority, err := GetPriorityByID(db.DefaultContext, priorityID) + if err != nil { + if IsErrPriorityNotExist(err) { + return nil + } + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + if priority.BelongsToOrg() && priority.OrgID != id { + return nil + } + if priority.BelongsToRepo() && priority.RepoID != id { + return nil + } + + if _, err = sess.ID(priorityID).Delete(new(Priority)); err != nil { + return err + } else if _, err = sess. + Where("label_id = ?", priorityID). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + if _, err = sess.Where("priority_id = ?", priorityID).Cols("priority_id").Delete(&Comment{}); err != nil { + return err + } + + return committer.Commit() +} + +// GetLabelByID returns a label by given ID. +func GetPriorityByID(ctx context.Context, priorityID int64) (*Priority, error) { + if priorityID <= 0 { + return nil, ErrLabelNotExist{priorityID} + } + + priority := &Priority{} + has, err := db.GetEngine(ctx).ID(priorityID).Get(priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPriorityNotExist{priority.ID} + } + return priority, nil +} + +// GetLabelsByIDs returns a list of labels by IDs +func GetPrioritiesByIDs(prioritiesIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(prioritiesIDs)) + return priorities, db.GetEngine(db.DefaultContext).Table("priorities"). + In("id", prioritiesIDs). + Asc("name"). + Cols("id", "repo_id", "org_id"). + Find(&priorities) +} + +// __________ .__ __ +// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. +// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | +// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | +// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| +// \/ \/|__| \/ \/ + +// GetLabelInRepoByName returns a label by name in given repository. +func GetPriorityInRepoByName(ctx context.Context, repoID int64, labelName string) (*Priority, error) { + if len(labelName) == 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + + priority := &Priority{ + Name: labelName, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{0, priority.RepoID} + } + return priority, nil +} + +// GetLabelInRepoByID returns a label by ID in given repository. +func GetPriorityInRepoByID(ctx context.Context, repoID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{priorityID, repoID} + } + + priority := &Priority{ + ID: priorityID, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{priority.ID, priority.RepoID} + } + return priority, nil +} + +// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given +// repository. +// it silently ignores label names that do not belong to the repository. +func GetPriorityIDsInRepoByNames(repoID int64, priorityNames []string) ([]int64, error) { + priorityIDs := make([]int64, 0, len(priorityNames)) + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("repo_id = ?", repoID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names +func BuildPriorityNamesIssueIDsCondition(priorityNames []string) *builder.Builder { + return builder.Select("issue_label.issue_id"). + From("issue"). + InnerJoin("priority", "priority.id = issue.priority_id"). + Where( + builder.In("priority.name", priorityNames), + ). + GroupBy("issue.id") +} + +// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, +// it silently ignores label IDs that do not belong to the repository. +func GetPriorityInRepoByIDs(repoID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("repo_id = ?", repoID). + In("id", priorities). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetPrioritiesByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + 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 = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByRepoID count number of all labels that belong to given repository by ID. +func CountPrioritiesByRepoID(repoID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Priority{}) +} + +// ________ +// \_____ \_______ ____ +// / | \_ __ \/ ___\ +// / | \ | \/ /_/ > +// \_______ /__| \___ / +// \/ /_____/ + +// GetLabelInOrgByName returns a label by name in given organization. +func GetPriorityInOrgByName(ctx context.Context, orgID int64, priorityName string) (*Priority, error) { + if len(priorityName) == 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + + priority := &Priority{ + Name: priorityName, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{0, priority.OrgID} + } + return priority, nil +} + +// GetLabelInOrgByID returns a label by ID in given organization. +func GetPriorityInOrgByID(ctx context.Context, orgID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{priorityID, orgID} + } + + priority := &Priority{ + ID: priorityID, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{priority.ID, priority.OrgID} + } + return priority, nil +} + +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given +// organization. +func GetPriorityIDsInOrgByNames(orgID int64, priorityNames []string) ([]int64, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorityIDs := make([]int64, 0, len(priorityNames)) + + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("org_id = ?", orgID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, +// it silently ignores label IDs that do not belong to the organization. +func GetPriorityInOrgByIDs(orgID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("org_id = ?", orgID). + In("id", priorityIDs). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByOrgID returns all labels that belong to given organization by ID. +func GetPrioritiesByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("org_id = ?", orgID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + 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 = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByOrgID count all labels that belong to given organization by ID. +func CountPrioritiesByOrgID(orgID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Priority{}) +} + +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ | +// \/ \/ \/ + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPrioritiesByIssueID(ctx context.Context, issueID int64) ([]*Priority, error) { + var priorities []*Priority + return priorities, db.GetEngine(ctx).Where("issue.id = ?", issueID). + Join("LEFT", "issue", "issue.priority_id = label.id"). + Asc("priority.name"). + Find(&priorities) +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPriorityByIssueID(ctx context.Context, issueID int64) (*Priority, error) { + priorities, err := GetPrioritiesByIssueID(ctx, issueID) + if err != nil { + return nil, err + } + if len(priorities) == 0 { + return nil, nil + } + return priorities[0], nil +} + +func updatePriorityCols(ctx context.Context, priority *Priority, cols ...string) error { + _, err := db.GetEngine(ctx).ID(priority.ID). + SetExpr("num_issues", + builder.Select("count(*)").From("priority"). + Where(builder.Eq{"id": priority.ID}), + ). + SetExpr("num_closed_issues", + builder.Select("count(*)").From("priority"). + InnerJoin("issue", "priority.id = issue.priority_id"). + Where(builder.Eq{ + "priority.id": priority.ID, + "issue.is_closed": true, + }), + ). + Cols(cols...).Update(priority) + return err +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssuePriority(ctx context.Context, issueID, priority int64) bool { + has, _ := db.GetEngine(ctx).Where("id = ? AND priority = ?", issueID, priority).Get(new(Issue)) + return has +} + +// newIssuePriority this function creates a new label it does not check if the label is valid for the issue +// YOU MUST CHECK THIS BEFORE THIS FUNCTION +func newIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = priority.ID + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + Content: "1", + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssuePriority(issue *Issue, priority *Priority, doer *user_model.User) (err error) { + if HasIssueLabel(db.DefaultContext, issue.ID, priority.ID) { + return nil + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + // Do NOT add invalid labels + if issue.RepoID != priority.RepoID && issue.Repo.OwnerID != priority.OrgID { + return nil + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + if err = issue.LoadPriorities(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue +func newIssuePriorities(ctx context.Context, issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return err + } + for _, priority := range priorities { + // Don't add already present labels and invalid labels + if HasIssueLabel(ctx, issue.ID, priority.ID) || + (priority.RepoID != issue.RepoID && priority.OrgID != issue.Repo.OwnerID) { + continue + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return fmt.Errorf("newIssuePriority: %v", err) + } + } + + return nil +} + +// NewIssueLabels creates a list of issue-label relations. +func NewIssuePriorities(issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = newIssuePriorities(ctx, issue, priorities, doer); err != nil { + return err + } + + if err = issue.LoadPriorities(ctx); err != nil { + issue.Priority = nil + return err + } + + return committer.Commit() +} + +func deleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = 0 + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) error { + if err := deleteIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + return issue.LoadPriorities(ctx) +} + +// DeleteLabelsByRepoID deletes labels of some repository +func DeletePrioritiesByRepoID(ctx context.Context, repoID int64) error { + _, err := db.DeleteByBean(ctx, &Priority{RepoID: repoID}) + return err +} + +// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore +func CountOrphanedPriorities() (int64, error) { + noref, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Count() + if err != nil { + return 0, err + } + + norepo, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Count() + if err != nil { + return 0, err + } + + noorg, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Count() + if err != nil { + return 0, err + } + + return noref + norepo + noorg, nil +} + +// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore +func DeleteOrphanedPriorities() error { + // delete labels with no reference + if _, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Priority)); err != nil { + return err + } + + // delete priorities with none existing repos + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Delete(Priority{}); err != nil { + return err + } + + // delete priorities with none existing orgs + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Delete(Priority{}); err != nil { + return err + } + + return nil +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 28ffc998860bf..7542fb2d81c8c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -413,6 +413,9 @@ var migrations = []Migration{ NewMigration("Add badges to users", createUserBadgesTable), // v225 -> v226 NewMigration("Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", alterPublicGPGKeyContentFieldsToMediumText), + + // v999 + NewMigration("Create issue priority tables", createIssuePriorityTables), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v999.go b/models/migrations/v999.go new file mode 100644 index 0000000000000..b693a7c48e89c --- /dev/null +++ b/models/migrations/v999.go @@ -0,0 +1,46 @@ +// Copyright 2022 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 ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createIssuePriorityTables(x *xorm.Engine) error { + // Priority represents a priority of repository for issues. + type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + if err := x.Sync2(new(Priority)); err != nil { + return err + } + + type Comment struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + if err := x.Sync2(new(Comment)); err != nil { + return err + } + + type Issue struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + + return x.Sync2(new(Issue)) +} diff --git a/modules/repository/init.go b/modules/repository/init.go index 37ed0748b4d34..aea8d6df833f4 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -471,3 +471,32 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } return nil } + +// InitializeLabels adds a label set to a repository using a template +func InitializePriorities(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { + list, err := GetLabelTemplateFile(labelTemplate) + if err != nil { + return err + } + + priorities := make([]*issues_model.Priority, len(list)) + for i := 0; i < len(list); i++ { + priorities[i] = &issues_model.Priority{ + Name: list[i][0], + Description: list[i][2], + Color: list[i][1], + Weight: 10, // FIXME: this should be configurable + } + if isOrg { + priorities[i].OrgID = id + } else { + priorities[i].RepoID = id + } + } + for _, priority := range priorities { + if err = issues_model.NewPriority(ctx, priority); err != nil { + return err + } + } + return nil +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 5dab770d55308..f51cd9a2154d4 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -338,6 +338,39 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.Data["Labels"] = labels ctx.Data["NumLabels"] = len(labels) + var prioritiesIDs []int64 + selectPriorities := ctx.FormString("priorities") + if len(selectPriorities) > 0 && selectPriorities != "0" { + labelIDs, err = base.StringsToInt64s(strings.Split(selectPriorities, ",")) + if err != nil { + ctx.ServerError("StringsToInt64s", err) + return + } + } + + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, repo.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByRepoID", err) + return + } + + if repo.Owner.IsOrganization() { + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByOrgID", err) + return + } + + ctx.Data["OrgLabels"] = orgPriorities + priorities = append(priorities, orgPriorities...) + } + + for _, p := range priorities { + p.LoadSelectedPrioritiesAfterClick(prioritiesIDs) + } + ctx.Data["Priorities"] = priorities + ctx.Data["NumPriorities"] = len(priorities) + if ctx.FormInt64("assignee") == 0 { assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. } @@ -691,48 +724,64 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is } // RetrieveRepoMetas find all the meta information of a repository -func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label { +func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) ([]*issues_model.Label, []*issues_model.Priority) { if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { - return nil + return nil, nil } labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) - return nil + return nil, nil } ctx.Data["Labels"] = labels if repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { - return nil + return nil, nil } ctx.Data["OrgLabels"] = orgLabels labels = append(labels, orgLabels...) } + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, repo.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByRepoID", err) + return nil, nil + } + ctx.Data["Priorities"] = priorities + if repo.Owner.IsOrganization() { + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + return nil, nil + } + + ctx.Data["OrgPriorities"] = orgPriorities + priorities = append(priorities, orgPriorities...) + } + RetrieveRepoMilestonesAndAssignees(ctx, repo) if ctx.Written() { - return nil + return nil, nil } retrieveProjects(ctx, repo) if ctx.Written() { - return nil + return nil, nil } brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) if err != nil { ctx.ServerError("GetBranches", err) - return nil + return nil, nil } ctx.Data["Branches"] = brs // Contains true if the user can create issue dependencies ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull) - return labels + return labels, priorities } func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error { @@ -928,15 +977,15 @@ func DeleteIssue(ctx *context.Context) { } // ValidateRepoMetas check and returns repository's meta information -func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { +func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64, int64) { var ( repo = ctx.Repo.Repository err error ) - labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) + labels, priorities := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) if ctx.Written() { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } var labelIDs []int64 @@ -945,7 +994,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.LabelIDs) > 0 { labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } labelIDMark := base.Int64sToMap(labelIDs) @@ -958,6 +1007,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull } ctx.Data["Labels"] = labels + ctx.Data["Priorities"] = priorities ctx.Data["HasSelectedLabel"] = hasSelected ctx.Data["label_ids"] = form.LabelIDs @@ -967,11 +1017,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if milestone.RepoID != repo.ID { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } ctx.Data["Milestone"] = milestone ctx.Data["milestone_id"] = milestoneID @@ -981,11 +1031,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull p, err := project_model.GetProjectByID(ctx, form.ProjectID) if err != nil { ctx.ServerError("GetProjectByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } ctx.Data["Project"] = p @@ -997,7 +1047,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.AssigneeIDs) > 0 { assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } // Check if the passed assignees actually exists and is assignable @@ -1005,18 +1055,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assignee, err := user_model.GetUserByID(aID) if err != nil { ctx.ServerError("GetUserByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull) if err != nil { ctx.ServerError("CanBeAssigned", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if !valid { ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } } } @@ -1026,7 +1076,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assigneeIDs = append(assigneeIDs, form.AssigneeID) } - return labelIDs, assigneeIDs, milestoneID, form.ProjectID + return labelIDs, assigneeIDs, milestoneID, form.ProjectID, form.Priority } // NewIssuePost response for creating new issue @@ -1044,7 +1094,7 @@ func NewIssuePost(ctx *context.Context) { attachments []string ) - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) + labelIDs, assigneeIDs, milestoneID, projectID, priority := ValidateRepoMetas(ctx, *form, false) if ctx.Written() { return } @@ -1079,6 +1129,7 @@ func NewIssuePost(ctx *context.Context) { MilestoneID: milestoneID, Content: content, Ref: form.Ref, + PriorityID: priority, } if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil { diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 7af415a8faed1..08659f98bcd61 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -66,7 +66,18 @@ func RetrieveLabels(ctx *context.Context) { l.CalOpenIssues() } + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("RetrieveLabels.GetPriorities", err) + return + } + + for _, p := range priorities { + p.CalOpenIssues() + } + ctx.Data["Labels"] = labels + ctx.Data["Priorities"] = priorities if ctx.Repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) @@ -79,6 +90,16 @@ func RetrieveLabels(ctx *context.Context) { } ctx.Data["OrgLabels"] = orgLabels + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByOrgID", err) + return + } + for _, p := range orgPriorities { + p.CalOpenOrgIssues(ctx.Repo.Repository.ID, p.ID) + } + ctx.Data["OrgPriorities"] = orgPriorities + org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName) if err != nil { ctx.ServerError("GetOrgByName", err) @@ -96,6 +117,7 @@ func RetrieveLabels(ctx *context.Context) { } } ctx.Data["NumLabels"] = len(labels) + ctx.Data["NumPriorities"] = len(priorities) ctx.Data["SortType"] = ctx.FormString("sort") } @@ -105,6 +127,10 @@ func NewLabel(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.labels") ctx.Data["PageIsLabels"] = true + if form.Priority { + NewPriority(ctx) + return + } if ctx.HasError() { ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) ctx.Redirect(ctx.Repo.RepoLink + "/labels") @@ -127,6 +153,10 @@ func NewLabel(ctx *context.Context) { // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) + if form.Priority { + UpdatePriority(ctx) + return + } l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) if err != nil { switch { diff --git a/routers/web/repo/issue_priority.go b/routers/web/repo/issue_priority.go new file mode 100644 index 0000000000000..dd33a686bbb2d --- /dev/null +++ b/routers/web/repo/issue_priority.go @@ -0,0 +1,165 @@ +// Copyright 2017 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 repo + +import ( + "net/http" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" + issue_service "code.gitea.io/gitea/services/issue" +) + +// InitializeLabels init labels for a repository +func InitializePriorities(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.InitializeLabelsForm) + if ctx.HasError() { + ctx.Redirect(ctx.Repo.RepoLink + "/labels") + return + } + + if err := repo_module.InitializeLabels(ctx, ctx.Repo.Repository.ID, form.TemplateName, false); err != nil { + if repo_module.IsErrIssueLabelTemplateLoad(err) { + originalErr := err.(repo_module.ErrIssueLabelTemplateLoad).OriginalError + ctx.Flash.Error(ctx.Tr("repo.issues.label_templates.fail_to_load_file", form.TemplateName, originalErr)) + ctx.Redirect(ctx.Repo.RepoLink + "/labels") + return + } + ctx.ServerError("InitializeLabels", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/labels") +} + +// NewLabel create new label for repository +func NewPriority(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.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.Repo.RepoLink + "/labels") + return + } + + p := &issues_model.Priority{ + RepoID: ctx.Repo.Repository.ID, + Name: form.Title, + Description: form.Description, + Color: form.Color, + Weight: form.Weight, + } + if err := issues_model.NewPriority(ctx, p); err != nil { + ctx.ServerError("NewPriority", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/labels") +} + +// UpdateLabel update a label's name and color +func UpdatePriority(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.CreateLabelForm) + p, err := issues_model.GetPriorityInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) + if err != nil { + switch { + case issues_model.IsErrRepoPriorityNotExist(err): + ctx.Error(http.StatusNotFound) + default: + ctx.ServerError("UpdatePriority", err) + } + return + } + + p.Name = form.Title + p.Description = form.Description + p.Weight = form.Weight + p.Color = form.Color + if err := issues_model.UpdatePriority(p); err != nil { + ctx.ServerError("UpdatePriority", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/labels") +} + +// DeleteLabel delete a label +func DeletePriority(ctx *context.Context) { + if err := issues_model.DeletePriority(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeletePriority: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.issues.priority_deletion_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": ctx.Repo.RepoLink + "/labels", + }) +} + +// UpdateIssueLabel change issue's labels +func UpdateIssuePriority(ctx *context.Context) { + issues := getActionIssues(ctx) + if ctx.Written() { + return + } + + switch action := ctx.FormString("action"); action { + case "clear": + for _, issue := range issues { + if err := issue_service.ClearLabels(issue, ctx.Doer); err != nil { + ctx.ServerError("ClearLabels", err) + return + } + } + case "attach", "detach", "toggle": + label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id")) + if err != nil { + if issues_model.IsErrRepoLabelNotExist(err) { + ctx.Error(http.StatusNotFound, "GetLabelByID") + } else { + ctx.ServerError("GetLabelByID", err) + } + return + } + + if action == "toggle" { + // detach if any issues already have label, otherwise attach + action = "attach" + for _, issue := range issues { + if issues_model.HasIssueLabel(ctx, issue.ID, label.ID) { + action = "detach" + break + } + } + } + + if action == "attach" { + for _, issue := range issues { + if err = issue_service.AddLabel(issue, ctx.Doer, label); err != nil { + ctx.ServerError("AddLabel", err) + return + } + } + } else { + for _, issue := range issues { + if err = issue_service.RemoveLabel(issue, ctx.Doer, label); err != nil { + ctx.ServerError("RemoveLabel", err) + return + } + } + } + default: + log.Warn("Unrecognized action: %s", action) + ctx.Error(http.StatusInternalServerError) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "ok": true, + }) +} diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index aa2c4cdb53b2d..5bf66faaa7687 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1164,7 +1164,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true) + labelIDs, assigneeIDs, milestoneID, _, _ := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { return } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c1e9cb3197c0b..72f3afc02e5a9 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -436,6 +436,7 @@ type CreateIssueForm struct { MilestoneID int64 ProjectID int64 AssigneeID int64 + Priority int64 Content string Files []string AllowMaintainerEdit bool @@ -560,6 +561,8 @@ type CreateLabelForm struct { ID int64 Title string `binding:"Required;MaxSize(50)" locale:"repo.issues.label_title"` Description string `binding:"MaxSize(200)" locale:"repo.issues.label_description"` + Weight int `binding:"MaxSize(200)" locale:"repo.issues.priority_weight"` + Priority bool `binding:"MaxSize(200)" locale:"repo.issues.priority_bool"` Color string `binding:"Required;MaxSize(7)" locale:"repo.issues.label_color"` } diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl index 88cfd124a722e..43071c3345350 100644 --- a/templates/repo/issue/labels.tmpl +++ b/templates/repo/issue/labels.tmpl @@ -7,16 +7,22 @@ {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
{{.locale.Tr "repo.issues.new_label"}}
+
{{.locale.Tr "repo.issues.new_priority"}}
{{end}}
{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} {{template "repo/issue/labels/label_new" .}} + {{template "repo/issue/priorities/priority_new" .}} {{end}} {{template "base/alert" .}} {{template "repo/issue/labels/label_list" .}} +
+
+ {{template "repo/issue/priorities/priority_list" .}} +
{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index ed1fd4778f7d2..622e5f79266a5 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -72,6 +72,43 @@
+ + + {{template "repo/issue/priorities/priority_sidebar" dict "root" $ "ctx" .}} + +
+ +{{template "base/footer" .}} diff --git a/templates/repo/issue/priorities/edit_delete_priority.tmpl b/templates/repo/issue/priorities/edit_delete_priority.tmpl new file mode 100644 index 0000000000000..fa84d26ffc9cd --- /dev/null +++ b/templates/repo/issue/priorities/edit_delete_priority.tmpl @@ -0,0 +1,58 @@ + + + diff --git a/templates/repo/issue/priorities/priority.tmpl b/templates/repo/issue/priorities/priority.tmpl new file mode 100644 index 0000000000000..7b4500570de64 --- /dev/null +++ b/templates/repo/issue/priorities/priority.tmpl @@ -0,0 +1,9 @@ + + {{.priority.Name | RenderEmoji}} + diff --git a/templates/repo/issue/priorities/priority_list.tmpl b/templates/repo/issue/priorities/priority_list.tmpl new file mode 100644 index 0000000000000..f8d3a73588e7e --- /dev/null +++ b/templates/repo/issue/priorities/priority_list.tmpl @@ -0,0 +1,96 @@ +

+ {{.locale.Tr "repo.issues.prority_count" .NumPriorities}} +
+ +
+

+ +
+
+ {{if and (not $.PageIsOrgSettingsPriorities) (or $.CanWriteIssues $.CanWritePulls) (eq .NumPriorities 0) (not $.Repository.IsArchived)}} + {{template "repo/issue/priorities/priority_load_template" .}} +
+ {{else if and ($.PageIsOrgSettingsPriorities) (eq .NumPriorities 0)}} + {{template "repo/issue/priorities/priority_load_template" .}} + {{end}} + {{range .Priorities}} +
  • +
    +
    +
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    +
    +
    +
    + {{.Description | RenderEmoji}} +
    +
    + + +
    +
  • + {{end}} + {{if and (not .PageIsOrgSettingsPriorities) (.OrgPriorities)}} +
  • +
    +
    + {{$.locale.Tr "repo.org_priorities_desc" | Str2html}} + {{if .IsOrganizationOwner}} + ({{$.locale.Tr "repo.org_priorities_desc_manage"}}): + {{end}} +
    +
    +
  • + {{if (not $.PageIsOrgSettingsPriorities)}} +
    + {{range .OrgPriorities}} +
  • +
    +
    +
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    +
    +
    +
    + {{.Description | RenderEmoji}} +
    +
    + +
    +
    +
    +
  • + {{end}} +
    + {{end}} + {{end}} +
    +
    diff --git a/templates/repo/issue/priorities/priority_load_template.tmpl b/templates/repo/issue/priorities/priority_load_template.tmpl new file mode 100644 index 0000000000000..defa9563c44ff --- /dev/null +++ b/templates/repo/issue/priorities/priority_load_template.tmpl @@ -0,0 +1,24 @@ +
    +
    +
    +

    {{.locale.Tr "repo.issues.priority_templates.info"}}

    +
    +
    + {{.CsrfTokenHtml}} +
    + +
    + +
    +
    +
    +
    diff --git a/templates/repo/issue/priorities/priority_new.tmpl b/templates/repo/issue/priorities/priority_new.tmpl new file mode 100644 index 0000000000000..b32d0edd7efa0 --- /dev/null +++ b/templates/repo/issue/priorities/priority_new.tmpl @@ -0,0 +1,33 @@ +
    +
    + {{.CsrfTokenHtml}} + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + {{template "repo/issue/label_precolors"}} +
    +
    +
    {{.locale.Tr "repo.milestones.cancel"}}
    + +
    +
    +
    +
    diff --git a/templates/repo/issue/priorities/priority_sidebar.tmpl b/templates/repo/issue/priorities/priority_sidebar.tmpl new file mode 100644 index 0000000000000..13b607bf42ab7 --- /dev/null +++ b/templates/repo/issue/priorities/priority_sidebar.tmpl @@ -0,0 +1,11 @@ +
    + {{.ctx.locale.Tr "repo.issues.new.no_priority"}} + + {{range .ctx.Priorities}} + {{template "repo/issue/priorities/priority" dict "root" $.root "priority" .}} + {{end}} + {{range .ctx.OrgPriorities}} + {{template "repo/issue/priorities/priority" dict "root" $.root "priority" .}} + {{end}} + +
    diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 5b95d3d8e56e8..99925eb7ab2a3 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -141,6 +141,41 @@
    + + {{template "repo/issue/priorities/priority_sidebar" dict "root" $ "ctx" .}} + +
    +