Add the ability to use multiple labels as filters(#5786)
This commit is contained in:
		
							parent
							
								
									6a949af8ca
								
							
						
					
					
						commit
						075649572d
					
				
					 9 changed files with 74 additions and 24 deletions
				
			
		|  | @ -1210,7 +1210,7 @@ type IssuesOptions struct { | |||
| 	PageSize    int | ||||
| 	IsClosed    util.OptionalBool | ||||
| 	IsPull      util.OptionalBool | ||||
| 	Labels      string | ||||
| 	LabelIDs    []int64 | ||||
| 	SortType    string | ||||
| 	IssueIDs    []int64 | ||||
| } | ||||
|  | @ -1289,15 +1289,10 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error { | |||
| 		sess.And("issue.is_pull=?", false) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(opts.Labels) > 0 && opts.Labels != "0" { | ||||
| 		labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if len(labelIDs) > 0 { | ||||
| 			sess. | ||||
| 				Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). | ||||
| 				In("issue_label.label_id", labelIDs) | ||||
| 	if opts.LabelIDs != nil { | ||||
| 		for i, labelID := range opts.LabelIDs { | ||||
| 			sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), | ||||
| 				fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
|  | @ -1475,9 +1470,11 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { | |||
| 			labelIDs, err := base.StringsToInt64s(strings.Split(opts.Labels, ",")) | ||||
| 			if err != nil { | ||||
| 				log.Warn("Malformed Labels argument: %s", opts.Labels) | ||||
| 			} else if len(labelIDs) > 0 { | ||||
| 				sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). | ||||
| 					In("issue_label.label_id", labelIDs) | ||||
| 			} else { | ||||
| 				for i, labelID := range labelIDs { | ||||
| 					sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), | ||||
| 						fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -69,6 +69,8 @@ type Label struct { | |||
| 	NumClosedIssues int | ||||
| 	NumOpenIssues   int  `xorm:"-"` | ||||
| 	IsChecked       bool `xorm:"-"` | ||||
| 	QueryString     string | ||||
| 	IsSelected      bool | ||||
| } | ||||
| 
 | ||||
| // APIFormat converts a Label to the api.Label format
 | ||||
|  | @ -85,6 +87,25 @@ func (label *Label) CalOpenIssues() { | |||
| 	label.NumOpenIssues = label.NumIssues - label.NumClosedIssues | ||||
| } | ||||
| 
 | ||||
| // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
 | ||||
| func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) { | ||||
| 	var labelQuerySlice []string | ||||
| 	labelSelected := false | ||||
| 	labelID := strconv.FormatInt(label.ID, 10) | ||||
| 	for _, s := range currentSelectedLabels { | ||||
| 		if s == label.ID { | ||||
| 			labelSelected = true | ||||
| 		} else if s > 0 { | ||||
| 			labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10)) | ||||
| 		} | ||||
| 	} | ||||
| 	if !labelSelected { | ||||
| 		labelQuerySlice = append(labelQuerySlice, labelID) | ||||
| 	} | ||||
| 	label.IsSelected = labelSelected | ||||
| 	label.QueryString = strings.Join(labelQuerySlice, ",") | ||||
| } | ||||
| 
 | ||||
| // ForegroundColor calculates the text color for labels based
 | ||||
| // on their background color.
 | ||||
| func (label *Label) ForegroundColor() template.CSS { | ||||
|  |  | |||
|  | @ -193,11 +193,19 @@ func TestIssues(t *testing.T) { | |||
| 		}, | ||||
| 		{ | ||||
| 			IssuesOptions{ | ||||
| 				Labels:   "1,2", | ||||
| 				LabelIDs: []int64{1}, | ||||
| 				Page:     1, | ||||
| 				PageSize: 4, | ||||
| 			}, | ||||
| 			[]int64{5, 2, 1}, | ||||
| 			[]int64{2, 1}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			IssuesOptions{ | ||||
| 				LabelIDs: []int64{1, 2}, | ||||
| 				Page:     1, | ||||
| 				PageSize: 4, | ||||
| 			}, | ||||
| 			[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
 | ||||
| 		}, | ||||
| 	} { | ||||
| 		issues, err := Issues(&test.Opts) | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -129,8 +129,11 @@ | |||
|             margin: 5px -7px 0 -5px; | ||||
|             width: 16px; | ||||
|         } | ||||
|         .text{ | ||||
|           margin-left: 0.9em; | ||||
|         &.labels .octicon { | ||||
|             margin: -2px -7px 0 -5px; | ||||
|         } | ||||
|         .text { | ||||
|             margin-left: 0.9em; | ||||
|         } | ||||
|         .menu { | ||||
|             max-height: 300px; | ||||
|  |  | |||
|  | @ -112,8 +112,15 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB | |||
| 	} | ||||
| 
 | ||||
| 	repo := ctx.Repo.Repository | ||||
| 	var labelIDs []int64 | ||||
| 	selectLabels := ctx.Query("labels") | ||||
| 
 | ||||
| 	if len(selectLabels) > 0 && selectLabels != "0" { | ||||
| 		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("StringsToInt64s", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	isShowClosed := ctx.Query("state") == "closed" | ||||
| 
 | ||||
| 	keyword := strings.Trim(ctx.Query("q"), " ") | ||||
|  | @ -176,7 +183,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB | |||
| 			PageSize:    setting.UI.IssuePagingNum, | ||||
| 			IsClosed:    util.OptionalBoolOf(isShowClosed), | ||||
| 			IsPull:      isPullOption, | ||||
| 			Labels:      selectLabels, | ||||
| 			LabelIDs:    labelIDs, | ||||
| 			SortType:    sortType, | ||||
| 			IssueIDs:    issueIDs, | ||||
| 		}) | ||||
|  | @ -210,7 +217,11 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB | |||
| 		ctx.ServerError("GetLabelsByRepoID", err) | ||||
| 		return | ||||
| 	} | ||||
| 	for _, l := range labels { | ||||
| 		l.LoadSelectedLabelsAfterClick(labelIDs) | ||||
| 	} | ||||
| 	ctx.Data["Labels"] = labels | ||||
| 	ctx.Data["NumLabels"] = len(labels) | ||||
| 
 | ||||
| 	if ctx.QueryInt64("assignee") == 0 { | ||||
| 		assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
 | ||||
|  |  | |||
|  | @ -656,7 +656,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 
 | ||||
| 	m.Group("/:username/:reponame", func() { | ||||
| 		m.Group("", func() { | ||||
| 			m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues) | ||||
| 			m.Get("/^:type(issues|pulls)$", repo.Issues) | ||||
| 			m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue) | ||||
| 			m.Get("/labels/", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels) | ||||
| 			m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones) | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
|  | @ -256,7 +257,16 @@ func Issues(ctx *context.Context) { | |||
| 
 | ||||
| 	opts.Page = page | ||||
| 	opts.PageSize = setting.UI.IssuePagingNum | ||||
| 	opts.Labels = ctx.Query("labels") | ||||
| 	var labelIDs []int64 | ||||
| 	selectLabels := ctx.Query("labels") | ||||
| 	if len(selectLabels) > 0 && selectLabels != "0" { | ||||
| 		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("StringsToInt64s", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	opts.LabelIDs = labelIDs | ||||
| 
 | ||||
| 	issues, err := models.Issues(opts) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="ten wide right aligned column"> | ||||
| 				<div class="ui secondary filter stackable menu"> | ||||
| 				<div class="ui secondary filter stackable menu labels"> | ||||
| 					<!-- Label --> | ||||
| 					<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item" style="margin-left: auto"> | ||||
| 						<span class="text"> | ||||
|  | @ -42,7 +42,7 @@ | |||
| 						<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}}</a> | ||||
| 								<a class="item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}"><span class="octicon {{if .IsSelected}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a> | ||||
| 							{{end}} | ||||
| 						</div> | ||||
| 					</div> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue