From a6e654dfd53f4d93f99523e3b07fd0add3fc65d1 Mon Sep 17 00:00:00 2001 From: Gitea Date: Sun, 12 Jan 2020 10:00:16 +0100 Subject: [PATCH 1/4] Add additional states (in-progress and review) for issues --- models/issue.go | 155 ++++++++++++++++++++++--- models/migrations/migrations.go | 4 + models/migrations/v103_1.go | 14 +++ models/migrations/v103_2.go | 17 +++ modules/auth/repo_form.go | 2 +- modules/structs/issue.go | 8 +- options/locale/locale_en-US.ini | 10 ++ public/js/index.js | 24 +++- routers/repo/issue.go | 48 +++++++- routers/user/home.go | 20 ++-- templates/repo/issue/list.tmpl | 37 ++++-- templates/repo/issue/view_content.tmpl | 35 ++++-- templates/repo/issue/view_title.tmpl | 4 + templates/user/dashboard/issues.tmpl | 10 +- 14 files changed, 334 insertions(+), 54 deletions(-) create mode 100644 models/migrations/v103_1.go create mode 100644 models/migrations/v103_2.go diff --git a/models/issue.go b/models/issue.go index 7c2eecadbcf35..8418c1e80832a 100644 --- a/models/issue.go +++ b/models/issue.go @@ -42,12 +42,13 @@ type Issue struct { 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 +356,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 +754,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 +852,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(doer *User, state api.StateType) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = issue.changeState(sess, doer, 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() @@ -1018,6 +1059,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) @@ -1280,6 +1322,7 @@ type IssuesOptions struct { MilestoneID int64 Page int PageSize int + StateType api.StateType IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 @@ -1332,6 +1375,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) @@ -1461,11 +1506,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. @@ -1553,7 +1598,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 +1645,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 +1673,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 +1705,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 +1733,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/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..681bff14ef2da 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -326,7 +326,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..e2073fa9e059a 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -14,6 +14,10 @@ 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 @@ -41,10 +45,10 @@ type Issue struct { 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..b095a7388c6ce 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -789,6 +789,8 @@ 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_label = Label issues.filter_label_no_select = All labels @@ -815,6 +817,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 +834,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 +843,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..2b15a39e4cd20 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -262,7 +262,7 @@ function initRepoStatusChecker() { location.reload(); return } - + setTimeout(function () { initRepoStatusChecker() }, 2000); @@ -931,17 +931,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..bb11c48f75c89 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -107,6 +107,11 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB var err error viewType := ctx.Query("type") sortType := ctx.Query("sort") + stateType := ctx.Query("state") + if stateType == "" { + stateType = "open" + } + types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned"} if !com.IsSliceContainsStr(types, viewType) { viewType = "all" @@ -201,6 +206,7 @@ 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, LabelIDs: labelIDs, @@ -269,11 +275,7 @@ 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") @@ -1144,12 +1146,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 +1184,12 @@ func UpdateIssueStatus(ctx *context.Context) { notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed) } + if issue.StateType != state { + if err := issue.ChangeState(ctx.User, state); err != nil { + ctx.ServerError("ChangeState", err) + return + } + } } ctx.JSON(200, map[string]interface{}{ "ok": true, @@ -1225,11 +1242,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 +1310,13 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } + if issue.StateType != state { + if err := issue.ChangeState(ctx.User, 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/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..1999b3f1f6701 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -27,19 +27,27 @@
-
+ -
+
-
+
- + {{.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,10 +156,17 @@