Skip to content

Commit 09fe4a2

Browse files
ethantkoenigbkcsoft
authored andcommitted
Batch updates for issues (#926)
1 parent 021904e commit 09fe4a2

File tree

11 files changed

+363
-131
lines changed

11 files changed

+363
-131
lines changed

cmd/web.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -466,17 +466,16 @@ func runWeb(ctx *cli.Context) error {
466466
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
467467
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)
468468

469-
m.Group("/:index", func() {
470-
m.Post("/label", repo.UpdateIssueLabel)
471-
m.Post("/milestone", repo.UpdateIssueMilestone)
472-
m.Post("/assignee", repo.UpdateIssueAssignee)
473-
}, reqRepoWriter)
474-
475469
m.Group("/:index", func() {
476470
m.Post("/title", repo.UpdateIssueTitle)
477471
m.Post("/content", repo.UpdateIssueContent)
478472
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
479473
})
474+
475+
m.Post("/labels", repo.UpdateIssueLabel, reqRepoWriter)
476+
m.Post("/milestone", repo.UpdateIssueMilestone, reqRepoWriter)
477+
m.Post("/assignee", repo.UpdateIssueAssignee, reqRepoWriter)
478+
m.Post("/status", repo.UpdateIssueStatus, reqRepoWriter)
480479
})
481480
m.Group("/comments/:id", func() {
482481
m.Post("", repo.UpdateCommentContent)

models/issue.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,16 @@ func GetIssueByID(id int64) (*Issue, error) {
10021002
return getIssueByID(x, id)
10031003
}
10041004

1005+
func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
1006+
issues := make([]*Issue, 0, 10)
1007+
return issues, e.In("id", issueIDs).Find(&issues)
1008+
}
1009+
1010+
// GetIssuesByIDs return issues with the given IDs.
1011+
func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
1012+
return getIssuesByIDs(x, issueIDs)
1013+
}
1014+
10051015
// IssuesOptions represents options of an issue.
10061016
type IssuesOptions struct {
10071017
RepoID int64

models/issue_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,19 @@ func TestIssueAPIURL(t *testing.T) {
4242
assert.NoError(t, err)
4343
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
4444
}
45+
46+
func TestGetIssuesByIDs(t *testing.T) {
47+
assert.NoError(t, PrepareTestDatabase())
48+
testSuccess := func(expectedIssueIDs []int64, nonExistentIssueIDs []int64) {
49+
issues, err := GetIssuesByIDs(append(expectedIssueIDs, nonExistentIssueIDs...))
50+
assert.NoError(t, err)
51+
actualIssueIDs := make([]int64, len(issues))
52+
for i, issue := range issues {
53+
actualIssueIDs[i] = issue.ID
54+
}
55+
assert.Equal(t, expectedIssueIDs, actualIssueIDs)
56+
57+
}
58+
testSuccess([]int64{1, 2, 3}, []int64{})
59+
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
60+
}

options/locale/locale_en-US.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,13 @@ issues.filter_sort.recentupdate = Recently updated
583583
issues.filter_sort.leastupdate = Least recently updated
584584
issues.filter_sort.mostcomment = Most commented
585585
issues.filter_sort.leastcomment = Least commented
586+
issues.action_open = Open
587+
issues.action_close = Close
588+
issues.action_label = Label
589+
issues.action_milestone = Milestone
590+
issues.action_milestone_no_select = No milestone
591+
issues.action_assignee = Assignee
592+
issues.action_assignee_no_select = No assignee
586593
issues.opened_by = opened %[1]s by <a href="%[2]s">%[3]s</a>
587594
issues.opened_by_fake = opened %[1]s by %[2]s
588595
issues.previous = Previous

public/css/index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,9 @@ footer .ui.language .menu {
22702270
#search-user-box .results .item img {
22712271
margin-right: 8px;
22722272
}
2273+
.issue-actions {
2274+
display: none;
2275+
}
22732276
.issue.list {
22742277
list-style: none;
22752278
padding-top: 15px;

public/js/index.js

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ function initEditForm() {
8787
}
8888

8989

90+
function updateIssuesMeta(url, action, issueIds, elementId, afterSuccess) {
91+
$.ajax({
92+
type: "POST",
93+
url: url,
94+
data: {
95+
"_csrf": csrf,
96+
"action": action,
97+
"issue_ids": issueIds,
98+
"id": elementId
99+
},
100+
success: afterSuccess
101+
})
102+
}
103+
90104
function initCommentForm() {
91105
if ($('.comment.form').length == 0) {
92106
return
@@ -100,14 +114,6 @@ function initCommentForm() {
100114
var $labelMenu = $('.select-label .menu');
101115
var hasLabelUpdateAction = $labelMenu.data('action') == 'update';
102116

103-
function updateIssueMeta(url, action, id) {
104-
$.post(url, {
105-
"_csrf": csrf,
106-
"action": action,
107-
"id": id
108-
});
109-
}
110-
111117
$('.select-label').dropdown('setting', 'onHide', function(){
112118
if (hasLabelUpdateAction) {
113119
location.reload();
@@ -119,13 +125,23 @@ function initCommentForm() {
119125
$(this).removeClass('checked');
120126
$(this).find('.octicon').removeClass('octicon-check');
121127
if (hasLabelUpdateAction) {
122-
updateIssueMeta($labelMenu.data('update-url'), "detach", $(this).data('id'));
128+
updateIssuesMeta(
129+
$labelMenu.data('update-url'),
130+
"detach",
131+
$labelMenu.data('issue-id'),
132+
$(this).data('id')
133+
);
123134
}
124135
} else {
125136
$(this).addClass('checked');
126137
$(this).find('.octicon').addClass('octicon-check');
127138
if (hasLabelUpdateAction) {
128-
updateIssueMeta($labelMenu.data('update-url'), "attach", $(this).data('id'));
139+
updateIssuesMeta(
140+
$labelMenu.data('update-url'),
141+
"attach",
142+
$labelMenu.data('issue-id'),
143+
$(this).data('id')
144+
);
129145
}
130146
}
131147

@@ -148,7 +164,12 @@ function initCommentForm() {
148164
});
149165
$labelMenu.find('.no-select.item').click(function () {
150166
if (hasLabelUpdateAction) {
151-
updateIssueMeta($labelMenu.data('update-url'), "clear", '');
167+
updateIssuesMeta(
168+
$labelMenu.data('update-url'),
169+
"clear",
170+
$labelMenu.data('issue-id'),
171+
""
172+
);
152173
}
153174

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

182203
$(this).addClass('selected active');
183204
if (hasUpdateAction) {
184-
updateIssueMeta($menu.data('update-url'), '', $(this).data('id'));
205+
updateIssuesMeta(
206+
$menu.data('update-url'),
207+
"",
208+
$menu.data('issue-id'),
209+
$(this).data('id')
210+
);
185211
}
186212
switch (input_id) {
187213
case '#milestone_id':
@@ -202,7 +228,12 @@ function initCommentForm() {
202228
});
203229

204230
if (hasUpdateAction) {
205-
updateIssueMeta($menu.data('update-url'), '', '');
231+
updateIssuesMeta(
232+
$menu.data('update-url'),
233+
"",
234+
$menu.data('issue-id'),
235+
$(this).data('id')
236+
);
206237
}
207238

208239
$list.find('.selected').html('');
@@ -1431,6 +1462,29 @@ $(document).ready(function () {
14311462
});
14321463
$('.markdown').autolink();
14331464

1465+
$('.issue-checkbox').click(function() {
1466+
var numChecked = $('.issue-checkbox').children('input:checked').length;
1467+
if (numChecked > 0) {
1468+
$('.issue-filters').hide();
1469+
$('.issue-actions').show();
1470+
} else {
1471+
$('.issue-filters').show();
1472+
$('.issue-actions').hide();
1473+
}
1474+
});
1475+
1476+
$('.issue-action').click(function () {
1477+
var action = this.dataset.action
1478+
var elementId = this.dataset.elementId
1479+
var issueIDs = $('.issue-checkbox').children('input:checked').map(function() {
1480+
return this.dataset.issueId;
1481+
}).get().join();
1482+
var url = this.dataset.url
1483+
updateIssuesMeta(url, action, issueIDs, elementId, function() {
1484+
location.reload();
1485+
});
1486+
});
1487+
14341488
buttonsClickOnEnter();
14351489
searchUsers();
14361490
searchRepositories();

public/less/_repository.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,10 @@
12611261
}
12621262
}
12631263

1264+
.issue-actions {
1265+
display: none;
1266+
}
1267+
12641268
.issue.list {
12651269
list-style: none;
12661270
padding-top: 15px;

routers/repo/issue.go

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"io/ioutil"
13+
"strconv"
1314
"strings"
1415
"time"
1516

@@ -644,6 +645,28 @@ func getActionIssue(ctx *context.Context) *models.Issue {
644645
return issue
645646
}
646647

648+
func getActionIssues(ctx *context.Context) []*models.Issue {
649+
commaSeparatedIssueIDs := ctx.Query("issue_ids")
650+
if len(commaSeparatedIssueIDs) == 0 {
651+
return nil
652+
}
653+
issueIDs := make([]int64, 0, 10)
654+
for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
655+
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
656+
if err != nil {
657+
ctx.Handle(500, "ParseInt", err)
658+
return nil
659+
}
660+
issueIDs = append(issueIDs, issueID)
661+
}
662+
issues, err := models.GetIssuesByIDs(issueIDs)
663+
if err != nil {
664+
ctx.Handle(500, "GetIssuesByIDs", err)
665+
return nil
666+
}
667+
return issues
668+
}
669+
647670
// UpdateIssueTitle change issue's title
648671
func UpdateIssueTitle(ctx *context.Context) {
649672
issue := getActionIssue(ctx)
@@ -697,25 +720,22 @@ func UpdateIssueContent(ctx *context.Context) {
697720

698721
// UpdateIssueMilestone change issue's milestone
699722
func UpdateIssueMilestone(ctx *context.Context) {
700-
issue := getActionIssue(ctx)
723+
issues := getActionIssues(ctx)
701724
if ctx.Written() {
702725
return
703726
}
704727

705-
oldMilestoneID := issue.MilestoneID
706728
milestoneID := ctx.QueryInt64("id")
707-
if oldMilestoneID == milestoneID {
708-
ctx.JSON(200, map[string]interface{}{
709-
"ok": true,
710-
})
711-
return
712-
}
713-
714-
// Not check for invalid milestone id and give responsibility to owners.
715-
issue.MilestoneID = milestoneID
716-
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
717-
ctx.Handle(500, "ChangeMilestoneAssign", err)
718-
return
729+
for _, issue := range issues {
730+
oldMilestoneID := issue.MilestoneID
731+
if oldMilestoneID == milestoneID {
732+
continue
733+
}
734+
issue.MilestoneID = milestoneID
735+
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
736+
ctx.Handle(500, "ChangeMilestoneAssign", err)
737+
return
738+
}
719739
}
720740

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

726746
// UpdateIssueAssignee change issue's assignee
727747
func UpdateIssueAssignee(ctx *context.Context) {
728-
issue := getActionIssue(ctx)
748+
issues := getActionIssues(ctx)
729749
if ctx.Written() {
730750
return
731751
}
732752

733753
assigneeID := ctx.QueryInt64("id")
734-
if issue.AssigneeID == assigneeID {
735-
ctx.JSON(200, map[string]interface{}{
736-
"ok": true,
737-
})
738-
return
754+
for _, issue := range issues {
755+
if issue.AssigneeID == assigneeID {
756+
continue
757+
}
758+
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
759+
ctx.Handle(500, "ChangeAssignee", err)
760+
return
761+
}
739762
}
763+
ctx.JSON(200, map[string]interface{}{
764+
"ok": true,
765+
})
766+
}
740767

741-
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
742-
ctx.Handle(500, "ChangeAssignee", err)
768+
// UpdateIssueStatus change issue's status
769+
func UpdateIssueStatus(ctx *context.Context) {
770+
issues := getActionIssues(ctx)
771+
if ctx.Written() {
743772
return
744773
}
745774

775+
var isClosed bool
776+
switch action := ctx.Query("action"); action {
777+
case "open":
778+
isClosed = false
779+
case "close":
780+
isClosed = true
781+
default:
782+
log.Warn("Unrecognized action: %s", action)
783+
}
784+
785+
if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
786+
ctx.Handle(500, "LoadRepositories", err)
787+
return
788+
}
789+
for _, issue := range issues {
790+
if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
791+
ctx.Handle(500, "ChangeStatus", err)
792+
return
793+
}
794+
}
746795
ctx.JSON(200, map[string]interface{}{
747796
"ok": true,
748797
})

0 commit comments

Comments
 (0)