Batch updates for issues (#926)
This commit is contained in:
		
							parent
							
								
									021904e4e6
								
							
						
					
					
						commit
						09fe4a2ae9
					
				
					 11 changed files with 365 additions and 133 deletions
				
			
		
							
								
								
									
										11
									
								
								cmd/web.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cmd/web.go
									
									
									
									
									
								
							|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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}) | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -1261,6 +1261,10 @@ | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .issue-actions { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .issue.list { | ||||
| 	list-style: none; | ||||
| 	padding-top: 15px; | ||||
|  |  | |||
|  | @ -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, | ||||
| 		}) | ||||
| 	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, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // UpdateIssueStatus change issue's status
 | ||||
| func UpdateIssueStatus(ctx *context.Context) { | ||||
| 	issues := getActionIssues(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil { | ||||
| 		ctx.Handle(500, "ChangeAssignee", err) | ||||
| 		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, | ||||
| 	}) | ||||
|  |  | |||
|  | @ -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{}{ | ||||
|  |  | |||
|  | @ -14,86 +14,147 @@ | |||
| 			</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 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}} | ||||
| 					</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}} | ||||
| 					</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> | ||||
| 
 | ||||
| 				<!-- 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="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-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> | ||||
| 
 | ||||
| 			<!-- 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 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> | ||||
| 			</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}} | ||||
| 				<!-- 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> | ||||
| 			</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> | ||||
| 
 | ||||
| 			<!-- 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> | ||||
| 				<!-- 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> | ||||
|  | @ -102,6 +163,9 @@ | |||
| 			{{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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -311,7 +311,7 @@ | |||
| 					<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> | ||||
|  | @ -335,7 +335,7 @@ | |||
| 					<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> | ||||
|  | @ -376,7 +376,7 @@ | |||
| 					<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> | ||||
|  | @ -442,4 +442,4 @@ | |||
| 		{{.i18n.Tr "repo.branch.delete_notices_2" .HeadTarget | Safe}}<br> | ||||
| 	</div> | ||||
| 	{{template "base/delete_modal_actions" .}} | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue