]> source.dussan.org Git - gitea.git/commitdiff
Batch updates for issues (#926)
authorEthan Koenig <etk39@cornell.edu>
Wed, 15 Mar 2017 01:10:35 +0000 (21:10 -0400)
committerKim "BKC" Carlbäcker <kim.carlbacker@gmail.com>
Wed, 15 Mar 2017 01:10:35 +0000 (02:10 +0100)
cmd/web.go
models/issue.go
models/issue_test.go
options/locale/locale_en-US.ini
public/css/index.css
public/js/index.js
public/less/_repository.less
routers/repo/issue.go
routers/repo/issue_label.go
templates/repo/issue/list.tmpl
templates/repo/issue/view_content.tmpl

index af32592d0b7afe513ee578ab5981905ddd62c211..0410ad5190ce15c5c89bb567e155e1b16625d06b 100644 (file)
@@ -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)
index eab494bdead14251d4a796552b29b1b6e1e99615..347598300e5fafd1dc3822e35407192539dad13d 100644 (file)
@@ -1002,6 +1002,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
index a6da80917e1065e4bd44147a2739cfb3f67634ba..7c80258c2b8d9ddaca1c31cc6e080463d44ba841 100644 (file)
@@ -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})
+}
index d39589bd3055418e05e192ef692708fc4d9bba89..ad75f28c973f1c179295f30eedcb27cd7e3c0f5c 100644 (file)
@@ -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
index 54a0c23727f19286d86d7ddfb4e3a653045cc8b6..e840659a21df3de2b6c969135e6c94b291f5b2c5 100644 (file)
@@ -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;
index 98a0efff47abecb4c1acfffa919712c639e68e55..e139d16748a942180066a3fd8bb9ca848c29fe99 100644 (file)
@@ -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
@@ -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();
@@ -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')
+                );
             }
         }
 
@@ -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 () {
@@ -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':
@@ -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('');
@@ -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();
index fcf6efda2782e6cc6d1f9fdb2d9cbc828c511dda..20098476704be7ebd7c140cf081fd0a78af4164b 100644 (file)
        }
 }
 
+.issue-actions {
+    display: none;
+}
+
 .issue.list {
        list-style: none;
        padding-top: 15px;
index a06f21b85968fa1b72ec6ed10adf77086ca86610..0a723d755b401eb61c37b9f21d97a9cbf4efc557 100644 (file)
@@ -10,6 +10,7 @@ import (
        "fmt"
        "io"
        "io/ioutil"
+       "strconv"
        "strings"
        "time"
 
@@ -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)
@@ -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{}{
@@ -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,
        })
index 6792947669f369613732485a0de55d2c83d52e09..966c2c1c535579109617cf66f1c90236e6c32d8f 100644 (file)
@@ -9,6 +9,7 @@ import (
        "code.gitea.io/gitea/modules/auth"
        "code.gitea.io/gitea/modules/base"
        "code.gitea.io/gitea/modules/context"
+       "code.gitea.io/gitea/modules/log"
 )
 
 const (
@@ -129,18 +130,20 @@ func DeleteLabel(ctx *context.Context) {
 
 // UpdateIssueLabel change issue's labels
 func UpdateIssueLabel(ctx *context.Context) {
-       issue := getActionIssue(ctx)
+       issues := getActionIssues(ctx)
        if ctx.Written() {
                return
        }
 
-       if ctx.Query("action") == "clear" {
-               if err := issue.ClearLabels(ctx.User); err != nil {
-                       ctx.Handle(500, "ClearLabels", err)
-                       return
+       switch action := ctx.Query("action"); action {
+       case "clear":
+               for _, issue := range issues {
+                       if err := issue.ClearLabels(ctx.User); err != nil {
+                               ctx.Handle(500, "ClearLabels", err)
+                               return
+                       }
                }
-       } else {
-               isAttach := ctx.Query("action") == "attach"
+       case "attach", "detach", "toggle":
                label, err := models.GetLabelByID(ctx.QueryInt64("id"))
                if err != nil {
                        if models.IsErrLabelNotExist(err) {
@@ -151,17 +154,40 @@ func UpdateIssueLabel(ctx *context.Context) {
                        return
                }
 
-               if isAttach && !issue.HasLabel(label.ID) {
-                       if err = issue.AddLabel(ctx.User, label); err != nil {
-                               ctx.Handle(500, "AddLabel", err)
-                               return
+               if action == "toggle" {
+                       anyHaveLabel := false
+                       for _, issue := range issues {
+                               if issue.HasLabel(label.ID) {
+                                       anyHaveLabel = true
+                                       break
+                               }
                        }
-               } else if !isAttach && issue.HasLabel(label.ID) {
-                       if err = issue.RemoveLabel(ctx.User, label); err != nil {
-                               ctx.Handle(500, "RemoveLabel", err)
-                               return
+                       if anyHaveLabel {
+                               action = "detach"
+                       } else {
+                               action = "attach"
+                       }
+               }
+
+               if action == "attach" {
+                       for _, issue := range issues {
+                               if err = issue.AddLabel(ctx.User, label); err != nil {
+                                       ctx.Handle(500, "AddLabel", err)
+                                       return
+                               }
+                       }
+               } else {
+                       for _, issue := range issues {
+                               if err = issue.RemoveLabel(ctx.User, label); err != nil {
+                                       ctx.Handle(500, "RemoveLabel", err)
+                                       return
+                               }
                        }
                }
+       default:
+               log.Warn("Unrecognized action: %s", action)
+               ctx.Error(500)
+               return
        }
 
        ctx.JSON(200, map[string]interface{}{
index 1b060cb231357ed70c5e80ffd226933063f25bc1..863ae6a74ddd82091d1d0ea84f059b95c2817b82 100644 (file)
                        </div>
                </div>
                <div class="ui divider"></div>
-               <div class="ui tiny basic status buttons">
-                       <a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
-                               <i class="octicon octicon-issue-opened"></i>
-                               {{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}}
-                       </a>
-                       <a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
-                               <i class="octicon octicon-issue-closed"></i>
-                               {{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}}
-                       </a>
-               </div>
-               <div class="ui right floated secondary filter menu">
-                       <!-- Label -->
-                       <div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item">
-                               <span class="text">
-                                       {{.i18n.Tr "repo.issues.filter_label"}}
-                                       <i class="dropdown icon"></i>
-                               </span>
-                               <div class="menu">
-                                       <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_label_no_select"}}</a>
-                                       {{range .Labels}}
-                                               <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}"><span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name | Sanitize}}</a>
-                                       {{end}}
-                               </div>
+               <div class="issue-filters">
+                       <div class="ui tiny basic status buttons">
+                               <a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
+                                       <i class="octicon octicon-issue-opened"></i>
+                                       {{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}}
+                               </a>
+                               <a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
+                                       <i class="octicon octicon-issue-closed"></i>
+                                       {{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}}
+                               </a>
                        </div>
+                       <div class="ui right floated secondary filter menu">
+                               <!-- Label -->
+                               <div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.filter_label"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_label_no_select"}}</a>
+                                               {{range .Labels}}
+                                                       <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}"><span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name | Sanitize}}</a>
+                                               {{end}}
+                                       </div>
+                               </div>
 
-                       <!-- Milestone -->
-                       <div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item">
-                               <span class="text">
-                                       {{.i18n.Tr "repo.issues.filter_milestone"}}
-                                       <i class="dropdown icon"></i>
-                               </span>
-                               <div class="menu">
-                                       <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_milestone_no_select"}}</a>
-                                       {{range .Milestones}}
-                                               <a class="{{if eq $.MilestoneID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&assignee={{$.AssigneeID}}">{{.Name | Sanitize}}</a>
-                                       {{end}}
+                               <!-- Milestone -->
+                               <div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.filter_milestone"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_milestone_no_select"}}</a>
+                                               {{range .Milestones}}
+                                                       <a class="{{if eq $.MilestoneID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&assignee={{$.AssigneeID}}">{{.Name | Sanitize}}</a>
+                                               {{end}}
+                                       </div>
                                </div>
-                       </div>
 
-                       <!-- Assignee -->
-                       <div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
-                               <span class="text">
-                                       {{.i18n.Tr "repo.issues.filter_assignee"}}
-                                       <i class="dropdown icon"></i>
-                               </span>
-                               <div class="menu">
-                                       <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}">{{.i18n.Tr "repo.issues.filter_assginee_no_select"}}</a>
-                                       {{range .Assignees}}
-                                               <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.ID}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</a>
-                                       {{end}}
+                               <!-- Assignee -->
+                               <div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.filter_assignee"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}">{{.i18n.Tr "repo.issues.filter_assginee_no_select"}}</a>
+                                               {{range .Assignees}}
+                                                       <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{.ID}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</a>
+                                               {{end}}
+                                       </div>
+                               </div>
+
+                               <!-- Type -->
+                               <div class="ui dropdown type jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.filter_type"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
+                                               <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
+                                               <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
+                                               <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+                                       </div>
                                </div>
-                       </div>
 
-                       <!-- Type -->
-                       <div class="ui dropdown type jump item">
-                               <span class="text">
-                                       {{.i18n.Tr "repo.issues.filter_type"}}
-                                       <i class="dropdown icon"></i>
-                               </span>
-                               <div class="menu">
-                                       <a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.all_issues"}}</a>
-                                       <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}}</a>
-                                       <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.created_by_you"}}</a>
-                                       <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_type.mentioning_you"}}</a>
+                               <!-- Sort -->
+                               <div class="ui dropdown type jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.filter_sort"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
+                                               <a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
+                                               <a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+                                               <a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+                                               <a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+                                               <a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+                                       </div>
                                </div>
                        </div>
+               </div>
+               <div class="issue-actions">
+                       <div class="ui basic status buttons">
+                               <div class="ui green active basic button issue-action" data-action="open" data-url="{{$.Link}}/status">{{.i18n.Tr "repo.issues.action_open"}}</div>
+                               <div class="ui red active basic button issue-action" data-action="close" data-url="{{$.Link}}/status">{{.i18n.Tr "repo.issues.action_close"}}</div>
+                       </div>
 
-                       <!-- Sort -->
-                       <div class="ui dropdown type jump item">
-                               <span class="text">
-                                       {{.i18n.Tr "repo.issues.filter_sort"}}
-                                       <i class="dropdown icon"></i>
-                               </span>
-                               <div class="menu">
-                                       <a class="{{if or (eq .SortType "latest") (not .SortType)}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</a>
-                                       <a class="{{if eq .SortType "oldest"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</a>
-                                       <a class="{{if eq .SortType "recentupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</a>
-                                       <a class="{{if eq .SortType "leastupdate"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</a>
-                                       <a class="{{if eq .SortType "mostcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.mostcomment"}}</a>
-                                       <a class="{{if eq .SortType "leastcomment"}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}">{{.i18n.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+                       <div class="ui secondary filter menu floated right">
+                               <!-- Labels -->
+                               <div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.action_label"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               {{range .Labels}}
+                                                       <div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.Link}}/labels">
+                                                               <span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name | Sanitize}}
+                                                       </div>
+                                               {{end}}
+                                       </div>
+                               </div>
+
+                               <!-- Milestone -->
+                               <div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.action_milestone"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/milestone">
+                                                 {{.i18n.Tr "repo.issues.action_milestone_no_select"}}
+                                               </div>
+                                               {{range .Milestones}}
+                                                       <div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.Link}}/milestone">
+                                                               {{.Name | Sanitize}}
+                                                       </div>
+                                               {{end}}
+                                       </div>
+                               </div>
+
+                               <!-- Assignee -->
+                               <div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
+                                       <span class="text">
+                                               {{.i18n.Tr "repo.issues.action_assignee"}}
+                                               <i class="dropdown icon"></i>
+                                       </span>
+                                       <div class="menu">
+                                               <div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/assignee">
+                                                       {{.i18n.Tr "repo.issues.action_assignee_no_select"}}
+                                               </div>
+                                               {{range .Assignees}}
+                                                       <div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.Link}}/assignee">
+                                                               <img src="{{.RelAvatarLink}}"> {{.Name}}
+                                                       </div>
+                                               {{end}}
+                                       </div>
                                </div>
                        </div>
                </div>
                        {{range .Issues}}
                                {{ $timeStr:= TimeSince .Created $.Lang }}
                                <li class="item">
+                                       <div class="ui checkbox issue-checkbox">
+                                               <input type="checkbox" data-issue-id={{.ID}}></input>
+                                       </div>
                                        <div class="ui {{if .IsRead}}black{{else}}green{{end}} label">#{{.Index}}</div>
                                        <a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
 
index 42e9f01c0b3075d93ff73d5b8b8fc077a61f8889..4e35614dbbb9d505565e5aa429e65feee2eddc57 100644 (file)
                                        <strong>{{.i18n.Tr "repo.issues.new.labels"}}</strong>
                                        <span class="octicon octicon-gear"></span>
                                </span>
-                               <div class="filter menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/label">
+                               <div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels">
                                        <div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_labels"}}</div>
                                        {{range .Labels}}
                                                <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon {{if .IsChecked}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a>
                                        <strong>{{.i18n.Tr "repo.issues.new.milestone"}}</strong>
                                        <span class="octicon octicon-gear"></span>
                                </span>
-                               <div class="menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/milestone">
+                               <div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
                                        <div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_milestone"}}</div>
                                        {{if .OpenMilestones}}
                                                <div class="divider"></div>
                                        <strong>{{.i18n.Tr "repo.issues.new.assignee"}}</strong>
                                        <span class="octicon octicon-gear"></span>
                                </span>
-                               <div class="menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/assignee">
+                               <div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
                                        <div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div>
                                        {{range .Assignees}}
                                                <div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
                {{.i18n.Tr "repo.branch.delete_notices_2" .HeadTarget | Safe}}<br>
        </div>
        {{template "base/delete_modal_actions" .}}
-</div>
\ No newline at end of file
+</div>