Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch updates for issues #926

Merged
merged 3 commits into from
Mar 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,17 +466,16 @@ func runWeb(ctx *cli.Context) error {
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)

m.Group("/:index", func() {
m.Post("/label", repo.UpdateIssueLabel)
m.Post("/milestone", repo.UpdateIssueMilestone)
m.Post("/assignee", repo.UpdateIssueAssignee)
}, reqRepoWriter)

m.Group("/:index", func() {
m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent)
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
})

m.Post("/labels", repo.UpdateIssueLabel, reqRepoWriter)
m.Post("/milestone", repo.UpdateIssueMilestone, reqRepoWriter)
m.Post("/assignee", repo.UpdateIssueAssignee, reqRepoWriter)
m.Post("/status", repo.UpdateIssueStatus, reqRepoWriter)
})
m.Group("/comments/:id", func() {
m.Post("", repo.UpdateCommentContent)
Expand Down
10 changes: 10 additions & 0 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,16 @@ func GetIssueByID(id int64) (*Issue, error) {
return getIssueByID(x, id)
}

func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
issues := make([]*Issue, 0, 10)
return issues, e.In("id", issueIDs).Find(&issues)
}

// GetIssuesByIDs return issues with the given IDs.
func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
return getIssuesByIDs(x, issueIDs)
}

// IssuesOptions represents options of an issue.
type IssuesOptions struct {
RepoID int64
Expand Down
16 changes: 16 additions & 0 deletions models/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,19 @@ func TestIssueAPIURL(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
}

func TestGetIssuesByIDs(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
testSuccess := func(expectedIssueIDs []int64, nonExistentIssueIDs []int64) {
issues, err := GetIssuesByIDs(append(expectedIssueIDs, nonExistentIssueIDs...))
assert.NoError(t, err)
actualIssueIDs := make([]int64, len(issues))
for i, issue := range issues {
actualIssueIDs[i] = issue.ID
}
assert.Equal(t, expectedIssueIDs, actualIssueIDs)

}
testSuccess([]int64{1, 2, 3}, []int64{})
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
}
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,13 @@ issues.filter_sort.recentupdate = Recently updated
issues.filter_sort.leastupdate = Least recently updated
issues.filter_sort.mostcomment = Most commented
issues.filter_sort.leastcomment = Least commented
issues.action_open = Open
issues.action_close = Close
issues.action_label = Label
issues.action_milestone = Milestone
issues.action_milestone_no_select = No milestone
issues.action_assignee = Assignee
issues.action_assignee_no_select = No assignee
issues.opened_by = opened %[1]s by <a href="%[2]s">%[3]s</a>
issues.opened_by_fake = opened %[1]s by %[2]s
issues.previous = Previous
Expand Down
3 changes: 3 additions & 0 deletions public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,9 @@ footer .ui.language .menu {
#search-user-box .results .item img {
margin-right: 8px;
}
.issue-actions {
display: none;
}
.issue.list {
list-style: none;
padding-top: 15px;
Expand Down
80 changes: 67 additions & 13 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ function initEditForm() {
}


function updateIssuesMeta(url, action, issueIds, elementId, afterSuccess) {
$.ajax({
type: "POST",
url: url,
data: {
"_csrf": csrf,
"action": action,
"issue_ids": issueIds,
"id": elementId
},
success: afterSuccess
})
}

function initCommentForm() {
if ($('.comment.form').length == 0) {
return
Expand All @@ -100,14 +114,6 @@ function initCommentForm() {
var $labelMenu = $('.select-label .menu');
var hasLabelUpdateAction = $labelMenu.data('action') == 'update';

function updateIssueMeta(url, action, id) {
$.post(url, {
"_csrf": csrf,
"action": action,
"id": id
});
}

$('.select-label').dropdown('setting', 'onHide', function(){
if (hasLabelUpdateAction) {
location.reload();
Expand All @@ -119,13 +125,23 @@ function initCommentForm() {
$(this).removeClass('checked');
$(this).find('.octicon').removeClass('octicon-check');
if (hasLabelUpdateAction) {
updateIssueMeta($labelMenu.data('update-url'), "detach", $(this).data('id'));
updateIssuesMeta(
$labelMenu.data('update-url'),
"detach",
$labelMenu.data('issue-id'),
$(this).data('id')
);
}
} else {
$(this).addClass('checked');
$(this).find('.octicon').addClass('octicon-check');
if (hasLabelUpdateAction) {
updateIssueMeta($labelMenu.data('update-url'), "attach", $(this).data('id'));
updateIssuesMeta(
$labelMenu.data('update-url'),
"attach",
$labelMenu.data('issue-id'),
$(this).data('id')
);
}
}

Expand All @@ -148,7 +164,12 @@ function initCommentForm() {
});
$labelMenu.find('.no-select.item').click(function () {
if (hasLabelUpdateAction) {
updateIssueMeta($labelMenu.data('update-url'), "clear", '');
updateIssuesMeta(
$labelMenu.data('update-url'),
"clear",
$labelMenu.data('issue-id'),
""
);
}

$(this).parent().find('.item').each(function () {
Expand Down Expand Up @@ -181,7 +202,12 @@ function initCommentForm() {

$(this).addClass('selected active');
if (hasUpdateAction) {
updateIssueMeta($menu.data('update-url'), '', $(this).data('id'));
updateIssuesMeta(
$menu.data('update-url'),
"",
$menu.data('issue-id'),
$(this).data('id')
);
}
switch (input_id) {
case '#milestone_id':
Expand All @@ -202,7 +228,12 @@ function initCommentForm() {
});

if (hasUpdateAction) {
updateIssueMeta($menu.data('update-url'), '', '');
updateIssuesMeta(
$menu.data('update-url'),
"",
$menu.data('issue-id'),
$(this).data('id')
);
}

$list.find('.selected').html('');
Expand Down Expand Up @@ -1431,6 +1462,29 @@ $(document).ready(function () {
});
$('.markdown').autolink();

$('.issue-checkbox').click(function() {
var numChecked = $('.issue-checkbox').children('input:checked').length;
if (numChecked > 0) {
$('.issue-filters').hide();
$('.issue-actions').show();
} else {
$('.issue-filters').show();
$('.issue-actions').hide();
}
});

$('.issue-action').click(function () {
var action = this.dataset.action
var elementId = this.dataset.elementId
var issueIDs = $('.issue-checkbox').children('input:checked').map(function() {
return this.dataset.issueId;
}).get().join();
var url = this.dataset.url
updateIssuesMeta(url, action, issueIDs, elementId, function() {
location.reload();
});
});

buttonsClickOnEnter();
searchUsers();
searchRepositories();
Expand Down
4 changes: 4 additions & 0 deletions public/less/_repository.less
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,10 @@
}
}

.issue-actions {
display: none;
}

.issue.list {
list-style: none;
padding-top: 15px;
Expand Down
93 changes: 71 additions & 22 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -644,6 +645,28 @@ func getActionIssue(ctx *context.Context) *models.Issue {
return issue
}

func getActionIssues(ctx *context.Context) []*models.Issue {
commaSeparatedIssueIDs := ctx.Query("issue_ids")
if len(commaSeparatedIssueIDs) == 0 {
return nil
}
issueIDs := make([]int64, 0, 10)
for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
if err != nil {
ctx.Handle(500, "ParseInt", err)
return nil
}
issueIDs = append(issueIDs, issueID)
}
issues, err := models.GetIssuesByIDs(issueIDs)
if err != nil {
ctx.Handle(500, "GetIssuesByIDs", err)
return nil
}
return issues
}

// UpdateIssueTitle change issue's title
func UpdateIssueTitle(ctx *context.Context) {
issue := getActionIssue(ctx)
Expand Down Expand Up @@ -697,25 +720,22 @@ func UpdateIssueContent(ctx *context.Context) {

// UpdateIssueMilestone change issue's milestone
func UpdateIssueMilestone(ctx *context.Context) {
issue := getActionIssue(ctx)
issues := getActionIssues(ctx)
if ctx.Written() {
return
}

oldMilestoneID := issue.MilestoneID
milestoneID := ctx.QueryInt64("id")
if oldMilestoneID == milestoneID {
ctx.JSON(200, map[string]interface{}{
"ok": true,
})
return
}

// Not check for invalid milestone id and give responsibility to owners.
issue.MilestoneID = milestoneID
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
ctx.Handle(500, "ChangeMilestoneAssign", err)
return
for _, issue := range issues {
oldMilestoneID := issue.MilestoneID
if oldMilestoneID == milestoneID {
continue
}
issue.MilestoneID = milestoneID
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
ctx.Handle(500, "ChangeMilestoneAssign", err)
return
}
}

ctx.JSON(200, map[string]interface{}{
Expand All @@ -725,24 +745,53 @@ func UpdateIssueMilestone(ctx *context.Context) {

// UpdateIssueAssignee change issue's assignee
func UpdateIssueAssignee(ctx *context.Context) {
issue := getActionIssue(ctx)
issues := getActionIssues(ctx)
if ctx.Written() {
return
}

assigneeID := ctx.QueryInt64("id")
if issue.AssigneeID == assigneeID {
ctx.JSON(200, map[string]interface{}{
"ok": true,
})
return
for _, issue := range issues {
if issue.AssigneeID == assigneeID {
continue
}
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
ctx.Handle(500, "ChangeAssignee", err)
return
}
}
ctx.JSON(200, map[string]interface{}{
"ok": true,
})
}

if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
ctx.Handle(500, "ChangeAssignee", err)
// UpdateIssueStatus change issue's status
func UpdateIssueStatus(ctx *context.Context) {
issues := getActionIssues(ctx)
if ctx.Written() {
return
}

var isClosed bool
switch action := ctx.Query("action"); action {
case "open":
isClosed = false
case "close":
isClosed = true
default:
log.Warn("Unrecognized action: %s", action)
}

if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
ctx.Handle(500, "LoadRepositories", err)
return
}
for _, issue := range issues {
if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
ctx.Handle(500, "ChangeStatus", err)
return
}
}
ctx.JSON(200, map[string]interface{}{
"ok": true,
})
Expand Down
Loading