diff --git a/models/issue.go b/models/issue.go index 7c2eecadbcf35..d6d083a6f0eec 100644 --- a/models/issue.go +++ b/models/issue.go @@ -39,15 +39,17 @@ type Issue struct { Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` Labels []*Label `xorm:"-"` + Severity string `xorm:"VARCHAR(6) INDEX DEFAULT 'Low'"` MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Priority int - AssigneeID int64 `xorm:"-"` - Assignee *User `xorm:"-"` - IsClosed bool `xorm:"INDEX"` - IsRead bool `xorm:"-"` - IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. - PullRequest *PullRequest `xorm:"-"` + StateType api.StateType `xorm:"VARCHAR(11) INDEX DEFAULT 'open'"` + AssigneeID int64 `xorm:"-"` + Assignee *User `xorm:"-"` + IsClosed bool `xorm:"INDEX"` + IsRead bool `xorm:"-"` + IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. + PullRequest *PullRequest `xorm:"-"` NumComments int Ref string @@ -355,10 +357,7 @@ func (issue *Issue) PatchURL() string { // State returns string representation of issue status. func (issue *Issue) State() api.StateType { - if issue.IsClosed { - return api.StateClosed - } - return api.StateOpen + return issue.StateType } // APIFormat assumes some fields assigned with values: @@ -756,11 +755,13 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (er issue.IsClosed = isClosed if isClosed { issue.ClosedUnix = timeutil.TimeStampNow() + issue.StateType = api.StateClosed } else { issue.ClosedUnix = 0 + issue.StateType = api.StateOpen } - if err = updateIssueCols(e, issue, "is_closed", "closed_unix"); err != nil { + if err = updateIssueCols(e, issue, "is_closed", "closed_unix", "state_type"); err != nil { return err } @@ -852,6 +853,47 @@ func (issue *Issue) ChangeStatus(doer *User, isClosed bool) (err error) { return nil } +func (issue *Issue) changeState(e *xorm.Session, state api.StateType) (err error) { + // Reload the issue + currentIssue, err := getIssueByID(e, issue.ID) + if err != nil { + return err + } + + // Nothing should be performed if current state is same as target state + if currentIssue.StateType == state { + return nil + } + + issue.StateType = state + + if err = updateIssueCols(e, issue, "state_type"); err != nil { + return err + } + + return nil +} + +// ChangeState changes issue state to open, in-progress, review or closed. +func (issue *Issue) ChangeState(state api.StateType) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = issue.changeState(sess, state); err != nil { + return err + } + + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + sess.Close() + + return nil +} + // ChangeTitle changes the title of this issue, as the given user. func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) { sess := x.NewSession() @@ -1010,6 +1052,7 @@ func (issue *Issue) GetLastEventLabelFake() string { type NewIssueOptions struct { Repo *Repository Issue *Issue + Severity string LabelIDs []int64 AssigneeIDs []int64 Attachments []string // In UUID format. @@ -1018,6 +1061,7 @@ type NewIssueOptions struct { func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) + opts.Issue.StateType = api.StateOpen if opts.Issue.MilestoneID > 0 { milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID) @@ -1185,6 +1229,7 @@ func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeI Repo: repo, Issue: issue, LabelIDs: labelIDs, + Severity: issue.Severity, Attachments: uuids, AssigneeIDs: assigneeIDs, }); err != nil { @@ -1280,9 +1325,11 @@ type IssuesOptions struct { MilestoneID int64 Page int PageSize int + StateType api.StateType IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 + Severity string SortType string IssueIDs []int64 } @@ -1332,6 +1379,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { sess.In("issue.repo_id", opts.RepoIDs) } + sess.And("issue.state_type=?", opts.StateType) + switch opts.IsClosed { case util.OptionalBoolTrue: sess.And("issue.is_closed=?", true) @@ -1354,6 +1403,10 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { And("issue_user.uid = ?", opts.MentionedID) } + if opts.Severity != "" { + sess.And("issue.severity=?", opts.Severity) + } + if opts.MilestoneID > 0 { sess.And("issue.milestone_id=?", opts.MilestoneID) } @@ -1461,11 +1514,11 @@ func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []*User) error { // IssueStats represents issue statistic information. type IssueStats struct { - OpenCount, ClosedCount int64 - YourRepositoriesCount int64 - AssignCount int64 - CreateCount int64 - MentionCount int64 + OpenCount, InProgressCount, ReviewCount, ClosedCount int64 + YourRepositoriesCount int64 + AssignCount int64 + CreateCount int64 + MentionCount int64 } // Filter modes. @@ -1490,6 +1543,7 @@ func parseCountResult(results []map[string][]byte) int64 { type IssueStatsOptions struct { RepoID int64 Labels string + Severity string MilestoneID int64 AssigneeID int64 MentionedID int64 @@ -1506,6 +1560,10 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { sess := x. Where("issue.repo_id = ?", opts.RepoID) + if opts.Severity != "" { + sess.In("issue.severity", opts.Severity) + } + if len(opts.IssueIDs) > 0 { sess.In("issue.id", opts.IssueIDs) } @@ -1553,7 +1611,19 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { var err error stats.OpenCount, err = countSession(opts). - And("issue.is_closed = ?", false). + And("issue.state_type = ?", api.StateOpen). + Count(new(Issue)) + if err != nil { + return stats, err + } + stats.InProgressCount, err = countSession(opts). + And("issue.state_type = ?", api.StateInProgress). + Count(new(Issue)) + if err != nil { + return stats, err + } + stats.ReviewCount, err = countSession(opts). + And("issue.state_type = ?", api.StateReview). Count(new(Issue)) if err != nil { return stats, err @@ -1588,6 +1658,21 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { switch opts.FilterMode { case FilterModeAll: stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateOpen). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.InProgressCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateInProgress). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ReviewCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateReview). And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) if err != nil { @@ -1601,6 +1686,23 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } case FilterModeAssign: stats.OpenCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateOpen). + Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.InProgressCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateInProgress). + Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ReviewCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateReview). Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). And("issue_assignees.assignee_id = ?", opts.UserID). Count(new(Issue)) @@ -1616,6 +1718,21 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } case FilterModeCreate: stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateOpen). + And("poster_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.InProgressCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateInProgress). + And("poster_id = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ReviewCount, err = x.Where(cond).And("is_closed = ?", false). + And("state_type = ?", api.StateReview). And("poster_id = ?", opts.UserID). Count(new(Issue)) if err != nil { @@ -1629,6 +1746,23 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } case FilterModeMention: stats.OpenCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateOpen). + Join("INNER", "issue_user", "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?", true). + And("issue_user.uid = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.InProgressCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateInProgress). + Join("INNER", "issue_user", "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?", true). + And("issue_user.uid = ?", opts.UserID). + Count(new(Issue)) + if err != nil { + return nil, err + } + stats.ReviewCount, err = x.Where(cond).And("issue.is_closed = ?", false). + And("state_type = ?", api.StateReview). Join("INNER", "issue_user", "issue.id = issue_user.issue_id and issue_user.is_mentioned = ?", true). And("issue_user.uid = ?", opts.UserID). Count(new(Issue)) diff --git a/models/issue_severity.go b/models/issue_severity.go new file mode 100644 index 0000000000000..4dd3d7c6dd3d0 --- /dev/null +++ b/models/issue_severity.go @@ -0,0 +1,32 @@ +package models + +import ( + "fmt" + "github.com/go-xorm/xorm" +) + +// ChangeSeverity changes severity of issue. +func ChangeSeverity(issue *Issue, doer *User) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = changeSeverity(sess, doer, issue); err != nil { + return err + } + + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + return nil +} + +func changeSeverity(e *xorm.Session, doer *User, issue *Issue) error { + if err := updateIssueCols(e, issue, "severity"); err != nil { + return err + } + + return nil +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 96f7a77589012..40720e94fd6c0 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -260,6 +260,10 @@ var migrations = []Migration{ NewMigration("change length of some external login users columns", changeSomeColumnsLengthOfExternalLoginUser), // v102 -> v103 NewMigration("update migration repositories' service type", dropColumnHeadUserNameOnPullRequest), + // v103 -> v103_1 + NewMigration("add state_type column to issue", addStateTypeColumnToIssue), + // v103_1 -> v103_2 + NewMigration("update state_type column from issue", updateStateTypeColumnFromIssue), } // Migrate database to current version diff --git a/models/migrations/v103_1.go b/models/migrations/v103_1.go new file mode 100644 index 0000000000000..313c5ce882cce --- /dev/null +++ b/models/migrations/v103_1.go @@ -0,0 +1,14 @@ +package migrations + +import ( + api "code.gitea.io/gitea/modules/structs" + "github.com/go-xorm/xorm" +) + +func addStateTypeColumnToIssue(x *xorm.Engine) (err error) { + type Issue struct { + StateType api.StateType `xorm:"VARCHAR(11) INDEX DEFAULT 'open'"` + } + + return x.Sync2(new(Issue)) +} diff --git a/models/migrations/v103_2.go b/models/migrations/v103_2.go new file mode 100644 index 0000000000000..b90caa1f23b21 --- /dev/null +++ b/models/migrations/v103_2.go @@ -0,0 +1,17 @@ +package migrations + +import ( + "code.gitea.io/gitea/modules/log" + "github.com/go-xorm/xorm" +) + +func updateStateTypeColumnFromIssue(x *xorm.Engine) (err error) { + if _, err := x.Exec("UPDATE issue SET state_type = 'closed' WHERE is_closed = true;"); err != nil { + log.Warn("UPDATE state_type: %v", err) + } + if _, err := x.Exec("UPDATE issue SET state_type = 'open' WHERE is_closed = false;"); err != nil { + log.Warn("UPDATE state_type: %v", err) + } + + return nil +} diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index a9985fdcbc8c8..c21478a094baa 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -312,6 +312,7 @@ type CreateIssueForm struct { LabelIDs string `form:"label_ids"` AssigneeIDs string `form:"assignee_ids"` Ref string `form:"ref"` + SeverityID string MilestoneID int64 AssigneeID int64 Content string @@ -326,7 +327,7 @@ func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) bi // CreateCommentForm form for creating comment type CreateCommentForm struct { Content string - Status string `binding:"OmitEmpty;In(reopen,close)"` + Status string `binding:"OmitEmpty;In(reopen,in-progress,review,close)"` Files []string } diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 58fd7344b4f27..a873fda767a92 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -14,12 +14,50 @@ type StateType string const ( // StateOpen pr is opend StateOpen StateType = "open" + // StateInProgress pr is in-progress + StateInProgress StateType = "in-progress" + // StateReview pr is review + StateReview StateType = "review" // StateClosed pr is closed StateClosed StateType = "closed" // StateAll is all StateAll StateType = "all" ) +// Severity issue severity type +type SeverityType struct { + Name string + Color string +} +type Severity string + +const ( + SeverityLow Severity = "Low" + SeverityMedium Severity = "Medium" + SeverityHigh Severity = "High" + SeverityUrgent Severity = "Urgent" +) + +var ( + SeverityTypeLow = SeverityType{Name: "Low", Color: "#009800"} + SeverityTypeMedium = SeverityType{Name: "Medium", Color: "#fbca04"} + SeverityTypeHigh = SeverityType{Name: "High", Color: "#eb6420"} + SeverityTypeUrgent = SeverityType{Name: "Urgent", Color: "#e11d21"} + + SeverityTypesMap = map[Severity]SeverityType{ + SeverityLow: SeverityTypeLow, + SeverityMedium: SeverityTypeMedium, + SeverityHigh: SeverityTypeHigh, + SeverityUrgent: SeverityTypeUrgent, + } + SeverityTypesSlice = []SeverityType{ + SeverityTypeLow, + SeverityTypeMedium, + SeverityTypeHigh, + SeverityTypeUrgent, + } +) + // PullRequestMeta PR info if an issue is a PR type PullRequestMeta struct { HasMerged bool `json:"merged"` @@ -38,13 +76,14 @@ type Issue struct { Title string `json:"title"` Body string `json:"body"` Labels []*Label `json:"labels"` + Severity string `json:"severity"` Milestone *Milestone `json:"milestone"` Assignee *User `json:"assignee"` Assignees []*User `json:"assignees"` - // Whether the issue is open or closed + // Whether the issue is open, in-progress, review or closed // // type: string - // enum: open,closed + // enum: open, in-progress, review, closed State StateType `json:"state"` Comments int `json:"comments"` // swagger:strfmt date-time diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 94ded93c59c83..027f9eac7f7c0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -754,6 +754,7 @@ ext_issues.desc = Link to an external issue tracker. issues.desc = Organize bug reports, tasks and milestones. issues.new = New Issue issues.new.title_empty = Title cannot be empty +issues.severity = Severity issues.new.labels = Labels issues.new.no_label = No Label issues.new.clear_labels = Clear labels @@ -789,7 +790,11 @@ issues.remove_self_assignment = `removed their assignment %s` issues.change_title_at = `changed title from %s to %s %s` issues.delete_branch_at = `deleted branch %s %s` issues.open_tab = %d Open +issues.in_progress_tab = %d In-Progress +issues.review_tab = %d Review issues.close_tab = %d Closed +issues.filter_severity = Severity +issues.filter_severity_no_select = All severities issues.filter_label = Label issues.filter_label_no_select = All labels issues.filter_milestone = Milestone @@ -815,6 +820,8 @@ issues.filter_sort.feweststars = Fewest stars issues.filter_sort.mostforks = Most forks issues.filter_sort.fewestforks = Fewest forks issues.action_open = Open +issues.action_in_progress = In-Progress +issues.action_review = Review issues.action_close = Close issues.action_label = Label issues.action_milestone = Milestone @@ -830,6 +837,8 @@ issues.closed_by_fake = closed %[1]s by %[2]s issues.previous = Previous issues.next = Next issues.open_title = Open +issues.in_progress_title = In-Progress +issues.review_title = Review issues.closed_title = Closed issues.num_comments = %d comments issues.commented_at = `commented %s` @@ -837,7 +846,11 @@ issues.delete_comment_confirm = Are you sure you want to delete this comment? issues.no_content = There is no content yet. issues.close_issue = Close issues.close_comment_issue = Comment and Close +issues.review_comment_issue = Comment and Review +issues.in_progress_comment_issue = Comment and In-Progress issues.reopen_issue = Reopen +issues.review_issue = Review +issues.in_progress_issue = In-Progress issues.reopen_comment_issue = Comment and Reopen issues.create_comment = Comment issues.closed_at = `closed %[2]s` diff --git a/public/js/index.js b/public/js/index.js index 966fc05eff753..0fe98b39caf91 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -262,7 +262,7 @@ function initRepoStatusChecker() { location.reload(); return } - + setTimeout(function () { initRepoStatusChecker() }, 2000); @@ -572,6 +572,10 @@ function initCommentForm() { ).then(reload); } switch (input_id) { + case '#severity_id': + $list.find('.selected').html('' + + htmlEncode($(this).text()) + ''); + break; case '#milestone_id': $list.find('.selected').html('' + htmlEncode($(this).text()) + ''); @@ -605,6 +609,7 @@ function initCommentForm() { } // Milestone and assignee + selectItem('.select-severity', '#severity_id'); selectItem('.select-milestone', '#milestone_id'); selectItem('.select-assignee', '#assignee_id'); } @@ -931,17 +936,39 @@ function initRepository() { // Change status const $statusButton = $('#status-button'); + const $statusButtonInProgress = $('#status-button-in-progress'); + const $statusButtonReview = $('#status-button-review'); + const $statusButtonClose = $('#status-button-close'); + $('#comment-form .edit_area').keyup(function () { if ($(this).val().length == 0) { $statusButton.text($statusButton.data('status')) + $statusButtonInProgress.text($statusButtonInProgress.data('status')) + $statusButtonReview.text($statusButtonReview.data('status')) + $statusButtonClose.text($statusButtonClose.data('status')) } else { $statusButton.text($statusButton.data('status-and-comment')) + $statusButtonInProgress.text($statusButtonInProgress.data('status-and-comment')) + $statusButtonReview.text($statusButtonReview.data('status-and-comment')) + $statusButtonClose.text($statusButtonClose.data('status-and-comment')) } }); $statusButton.click(function () { $('#status').val($statusButton.data('status-val')); $('#comment-form').submit(); }); + $statusButtonInProgress.click(function () { + $('#status').val($statusButtonInProgress.data('status-val')); + $('#comment-form').submit(); + }); + $statusButtonReview.click(function () { + $('#status').val($statusButtonReview.data('status-val')); + $('#comment-form').submit(); + }); + $statusButtonClose.click(function () { + $('#status').val($statusButtonClose.data('status-val')); + $('#comment-form').submit(); + }); // Pull Request merge button const $mergeButton = $('.merge-button > button'); diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 8cbad47f2d8a6..aba6f7a28796a 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -107,6 +107,12 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB var err error viewType := ctx.Query("type") sortType := ctx.Query("sort") + severity := ctx.Query("severity") + stateType := ctx.Query("state") + if stateType == "" { + stateType = "open" + } + types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned"} if !com.IsSliceContainsStr(types, viewType) { viewType = "all" @@ -163,6 +169,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB } else { issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{ RepoID: repo.ID, + Severity: severity, Labels: selectLabels, MilestoneID: milestoneID, AssigneeID: assigneeID, @@ -182,9 +189,14 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB } var total int - if !isShowClosed { + switch api.StateType(stateType) { + case api.StateOpen: total = int(issueStats.OpenCount) - } else { + case api.StateInProgress: + total = int(issueStats.InProgressCount) + case api.StateReview: + total = int(issueStats.ReviewCount) + case api.StateClosed: total = int(issueStats.ClosedCount) } pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) @@ -201,8 +213,10 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB MilestoneID: milestoneID, Page: pager.Paginater.Current(), PageSize: setting.UI.IssuePagingNum, + StateType: api.StateType(stateType), IsClosed: util.OptionalBoolOf(isShowClosed), IsPull: isPullOption, + Severity: severity, LabelIDs: labelIDs, SortType: sortType, IssueIDs: issueIDs, @@ -261,6 +275,8 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB } ctx.Data["IssueStats"] = issueStats + ctx.Data["Severities"] = api.SeverityTypesSlice + ctx.Data["Severity"] = severity ctx.Data["SelLabelIDs"] = labelIDs ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType @@ -269,16 +285,13 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB ctx.Data["AssigneeID"] = assigneeID ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["Keyword"] = keyword - if isShowClosed { - ctx.Data["State"] = "closed" - } else { - ctx.Data["State"] = "open" - } + ctx.Data["State"] = stateType pager.AddParam(ctx, "q", "Keyword") pager.AddParam(ctx, "type", "ViewType") pager.AddParam(ctx, "sort", "SortType") pager.AddParam(ctx, "state", "State") + pager.AddParam(ctx, "severity", "Severity") pager.AddParam(ctx, "labels", "SelectLabels") pager.AddParam(ctx, "milestone", "MilestoneID") pager.AddParam(ctx, "assignee", "AssigneeID") @@ -426,6 +439,8 @@ func NewIssue(ctx *context.Context) { ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes body := ctx.Query("body") ctx.Data["BodyQuery"] = body + ctx.Data["Severities"] = api.SeverityTypesSlice + ctx.Data["Severity"] = api.SeverityTypesMap[api.SeverityLow] milestoneID := ctx.QueryInt64("milestone") if milestoneID > 0 { @@ -544,8 +559,9 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { repo = ctx.Repo.Repository attachments []string ) - + severity := form.SeverityID labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form, false) + if ctx.Written() { return } @@ -570,6 +586,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { PosterID: ctx.User.ID, Poster: ctx.User, MilestoneID: milestoneID, + Severity: severity, Content: form.Content, Ref: form.Ref, } @@ -696,6 +713,11 @@ func ViewIssue(ctx *context.Context) { } // Metas. + // Check severities. + severity := api.Severity(issue.Severity) + ctx.Data["Severities"] = api.SeverityTypesSlice + ctx.Data["Severity"] = api.SeverityTypesMap[severity] + // Check labels. labelIDMark := make(map[int64]bool) for i := range issue.Labels { @@ -1083,6 +1105,30 @@ func UpdateIssueContent(ctx *context.Context) { }) } +// UpdateIssueSeverity change issue's severity +func UpdateIssueSeverity(ctx *context.Context) { + issues := getActionIssues(ctx) + if ctx.Written() { + return + } + + severityID := ctx.Query("id") + for _, issue := range issues { + oldSeverityID := issue.Severity + if oldSeverityID == severityID { + continue + } + issue.Severity = severityID + if err := models.ChangeSeverity(issue, ctx.User); err != nil { + return + } + } + + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + // UpdateIssueMilestone change issue's milestone func UpdateIssueMilestone(ctx *context.Context) { issues := getActionIssues(ctx) @@ -1144,12 +1190,21 @@ func UpdateIssueStatus(ctx *context.Context) { return } + var state api.StateType var isClosed bool switch action := ctx.Query("action"); action { case "open": isClosed = false + state = api.StateOpen + case "in-progress": + isClosed = false + state = api.StateInProgress + case "review": + isClosed = false + state = api.StateReview case "close": isClosed = true + state = api.StateClosed default: log.Warn("Unrecognized action: %s", action) } @@ -1173,6 +1228,12 @@ func UpdateIssueStatus(ctx *context.Context) { notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed) } + if issue.StateType != state { + if err := issue.ChangeState(state); err != nil { + ctx.ServerError("ChangeState", err) + return + } + } } ctx.JSON(200, map[string]interface{}{ "ok": true, @@ -1225,11 +1286,23 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } + var state api.StateType + switch form.Status { + case "reopen": + state = api.StateOpen + case "in-progress": + state = api.StateInProgress + case "review": + state = api.StateReview + case "close": + state = api.StateClosed + } + var comment *models.Comment defer func() { // Check if issue admin/poster changes the status of issue. if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) && - (form.Status == "reopen" || form.Status == "close") && + (form.Status == "reopen" || form.Status == "close" || form.Status == "in-progress" || form.Status == "review") && !(issue.IsPull && issue.PullRequest.HasMerged) { // Duplication and conflict check should apply to reopen pull request. @@ -1281,6 +1354,13 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } + if issue.StateType != state { + if err := issue.ChangeState(state); err != nil { + ctx.ServerError("ChangeState", err) + return + } + } + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 8dfcdb9c9b64c..5c2cbbcf7c8cf 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -712,6 +712,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue) }, context.RepoMustNotBeArchived()) + m.Post("/severity", reqRepoIssuesOrPullsWriter, repo.UpdateIssueSeverity) m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) diff --git a/routers/user/home.go b/routers/user/home.go index 40b3bc3fc1b81..4d1e9f5f11a34 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -7,6 +7,7 @@ package user import ( "bytes" + api "code.gitea.io/gitea/modules/structs" "fmt" "sort" "strings" @@ -196,7 +197,10 @@ func Issues(ctx *context.Context) { repoID := ctx.QueryInt64("repo") isShowClosed := ctx.Query("state") == "closed" - + stateType := ctx.Query("state") + if stateType == "" { + stateType = "open" + } // Get repositories. var err error var userRepoIDs []int64 @@ -227,9 +231,10 @@ func Issues(ctx *context.Context) { } opts := &models.IssuesOptions{ - IsClosed: util.OptionalBoolOf(isShowClosed), - IsPull: util.OptionalBoolOf(isPullList), - SortType: sortType, + StateType: api.StateType(stateType), + IsClosed: util.OptionalBoolOf(isShowClosed), + IsPull: util.OptionalBoolOf(isPullList), + SortType: sortType, } if repoID > 0 { @@ -369,12 +374,7 @@ func Issues(ctx *context.Context) { ctx.Data["SortType"] = sortType ctx.Data["RepoID"] = repoID ctx.Data["IsShowClosed"] = isShowClosed - - if isShowClosed { - ctx.Data["State"] = "closed" - } else { - ctx.Data["State"] = "open" - } + ctx.Data["State"] = stateType pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) pager.AddParam(ctx, "type", "ViewType") diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 5572df671e260..114b5b72a9539 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -27,30 +27,53 @@
-
+ -
+ {{end}} @@ -106,27 +129,35 @@
-
+
- + {{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}} - + + + {{.i18n.Tr "repo.issues.in_progress_tab" .IssueStats.InProgressCount}} + + + + {{.i18n.Tr "repo.issues.review_tab" .IssueStats.ReviewCount}} + + {{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}} @@ -140,11 +171,32 @@