mirror of
https://github.com/go-gitea/gitea.git
synced 2024-12-28 00:17:00 +00:00
Add label/author/assignee filters to the user/org home issue list (#32779)
Replace #26661, fix #25979 Not perfect, but usable and much better than before. Since it is quite complex, I am not quite sure whether there would be any regression, if any, I will fix in first time. I have tested the related pages many times: issue list, milestone issue list, project view, user issue list, org issue list.
This commit is contained in:
parent
734ddf7118
commit
e619384098
@ -26,8 +26,10 @@ const (
|
|||||||
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// NoConditionID means a condition to filter the records which don't match any id.
|
||||||
// Which means a condition to filter the records which don't match any id.
|
// eg: "milestone_id=-1" means "find the items without any milestone.
|
||||||
// It's different from zero which means the condition could be ignored.
|
const NoConditionID int64 = -1
|
||||||
NoConditionID = -1
|
|
||||||
)
|
// NonExistingID means a condition to match no result (eg: a non-existing user)
|
||||||
|
// It doesn't use -1 or -2 because they are used as builtin users.
|
||||||
|
const NonExistingID int64 = -1000000
|
||||||
|
@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint
|
|||||||
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||||
AllPublic bool // include also all public repositories
|
AllPublic bool // include also all public repositories
|
||||||
RepoCond builder.Cond
|
RepoCond builder.Cond
|
||||||
AssigneeID int64
|
AssigneeID optional.Option[int64]
|
||||||
PosterID int64
|
PosterID optional.Option[int64]
|
||||||
MentionedID int64
|
MentionedID int64
|
||||||
ReviewRequestedID int64
|
ReviewRequestedID int64
|
||||||
ReviewedID int64
|
ReviewedID int64
|
||||||
@ -231,15 +231,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
|
|||||||
sess.And("issue.is_closed=?", opts.IsClosed.Value())
|
sess.And("issue.is_closed=?", opts.IsClosed.Value())
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
applyPosterCondition(sess, opts.PosterID)
|
||||||
} else if opts.AssigneeID == db.NoConditionID {
|
|
||||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.PosterID > 0 {
|
|
||||||
applyPosterCondition(sess, opts.PosterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.MentionedID > 0 {
|
if opts.MentionedID > 0 {
|
||||||
applyMentionedCondition(sess, opts.MentionedID)
|
applyMentionedCondition(sess, opts.MentionedID)
|
||||||
@ -359,13 +352,27 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
|
|||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
|
func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) {
|
||||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
|
||||||
And("issue_assignees.assignee_id = ?", assigneeID)
|
if !assigneeID.Has() || assigneeID.Value() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if assigneeID.Value() == db.NoConditionID {
|
||||||
|
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
||||||
|
} else {
|
||||||
|
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||||
|
And("issue_assignees.assignee_id = ?", assigneeID.Value())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyPosterCondition(sess *xorm.Session, posterID int64) {
|
func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) {
|
||||||
sess.And("issue.poster_id=?", posterID)
|
if !posterID.Has() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// poster doesn't need to support db.NoConditionID(-1), so just use the value as-is
|
||||||
|
if posterID.Has() {
|
||||||
|
sess.And("issue.poster_id=?", posterID.Value())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
|
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
|
||||||
|
@ -151,15 +151,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
|
|||||||
|
|
||||||
applyProjectCondition(sess, opts)
|
applyProjectCondition(sess, opts)
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
|
||||||
} else if opts.AssigneeID == db.NoConditionID {
|
|
||||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.PosterID > 0 {
|
applyPosterCondition(sess, opts.PosterID)
|
||||||
applyPosterCondition(sess, opts.PosterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.MentionedID > 0 {
|
if opts.MentionedID > 0 {
|
||||||
applyMentionedCondition(sess, opts.MentionedID)
|
applyMentionedCondition(sess, opts.MentionedID)
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -155,7 +156,7 @@ func TestIssues(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
issues_model.IssuesOptions{
|
issues_model.IssuesOptions{
|
||||||
AssigneeID: 1,
|
AssigneeID: optional.Some(int64(1)),
|
||||||
SortType: "oldest",
|
SortType: "oldest",
|
||||||
},
|
},
|
||||||
[]int64{1, 6},
|
[]int64{1, 6},
|
||||||
|
@ -54,8 +54,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||||||
RepoIDs: options.RepoIDs,
|
RepoIDs: options.RepoIDs,
|
||||||
AllPublic: options.AllPublic,
|
AllPublic: options.AllPublic,
|
||||||
RepoCond: nil,
|
RepoCond: nil,
|
||||||
AssigneeID: convertID(options.AssigneeID),
|
AssigneeID: optional.Some(convertID(options.AssigneeID)),
|
||||||
PosterID: convertID(options.PosterID),
|
PosterID: options.PosterID,
|
||||||
MentionedID: convertID(options.MentionID),
|
MentionedID: convertID(options.MentionID),
|
||||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||||
ReviewedID: convertID(options.ReviewedID),
|
ReviewedID: convertID(options.ReviewedID),
|
||||||
|
@ -40,14 +40,14 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||||||
|
|
||||||
if opts.ProjectID > 0 {
|
if opts.ProjectID > 0 {
|
||||||
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
||||||
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
|
} else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places
|
||||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.AssigneeID > 0 {
|
if opts.AssigneeID.Value() == db.NoConditionID {
|
||||||
searchOpt.AssigneeID = optional.Some(opts.AssigneeID)
|
searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee"
|
||||||
} else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places
|
} else if opts.AssigneeID.Value() != 0 {
|
||||||
searchOpt.AssigneeID = optional.Some[int64](0)
|
searchOpt.AssigneeID = opts.AssigneeID
|
||||||
}
|
}
|
||||||
|
|
||||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||||
@ -62,7 +62,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||||
searchOpt.PosterID = convertID(opts.PosterID)
|
searchOpt.PosterID = opts.PosterID
|
||||||
searchOpt.MentionID = convertID(opts.MentionedID)
|
searchOpt.MentionID = convertID(opts.MentionedID)
|
||||||
searchOpt.ReviewedID = convertID(opts.ReviewedID)
|
searchOpt.ReviewedID = convertID(opts.ReviewedID)
|
||||||
searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID)
|
searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID)
|
||||||
|
@ -191,7 +191,7 @@ func searchIssueByID(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
||||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: -1}),
|
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
|
||||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
@ -334,23 +335,15 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
|
||||||
// 1,-2 means including label 1 and excluding label 2
|
if ctx.Written() {
|
||||||
// 0 means issues with no label
|
return
|
||||||
// blank means labels will not be filtered for issues
|
|
||||||
selectLabels := ctx.FormString("labels")
|
|
||||||
if selectLabels != "" {
|
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
|
||||||
if err != nil {
|
|
||||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||||
assigneeID := ctx.FormInt64("assignee")
|
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: optional.Some(assigneeID),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
@ -426,8 +419,6 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
ctx.Data["SelectLabels"] = selectLabels
|
|
||||||
ctx.Data["AssigneeID"] = assigneeID
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
|
|
||||||
project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description)
|
project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description)
|
||||||
|
@ -17,12 +17,12 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
@ -263,8 +263,10 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
|
|||||||
return user.ID
|
return user.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListIssues list the issues of a repository
|
// SearchRepoIssuesJSON lists the issues of a repository
|
||||||
func ListIssues(ctx *context.Context) {
|
// This function was copied from API (decouple the web and API routes),
|
||||||
|
// it is only used by frontend to search some dependency or related issues
|
||||||
|
func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
||||||
@ -286,20 +288,11 @@ func ListIssues(ctx *context.Context) {
|
|||||||
keyword = ""
|
keyword = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
|
||||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
|
||||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mileIDs []int64
|
var mileIDs []int64
|
||||||
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
||||||
for i := range part {
|
for i := range part {
|
||||||
// uses names and fall back to ids
|
// uses names and fall back to ids
|
||||||
// non existent milestones are discarded
|
// non-existent milestones are discarded
|
||||||
mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
|
mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
mileIDs = append(mileIDs, mile.ID)
|
mileIDs = append(mileIDs, mile.ID)
|
||||||
@ -370,17 +363,8 @@ func ListIssues(ctx *context.Context) {
|
|||||||
if before != 0 {
|
if before != 0 {
|
||||||
searchOpt.UpdatedBeforeUnix = optional.Some(before)
|
searchOpt.UpdatedBeforeUnix = optional.Some(before)
|
||||||
}
|
}
|
||||||
if len(labelIDs) == 1 && labelIDs[0] == 0 {
|
|
||||||
searchOpt.NoLabelOnly = true
|
// TODO: the "labels" query parameter is never used, so no need to handle it
|
||||||
} else {
|
|
||||||
for _, labelID := range labelIDs {
|
|
||||||
if labelID > 0 {
|
|
||||||
searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
|
|
||||||
} else {
|
|
||||||
searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
|
if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
|
||||||
searchOpt.MilestoneIDs = []int64{0}
|
searchOpt.MilestoneIDs = []int64{0}
|
||||||
@ -503,8 +487,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
if !util.SliceContainsString(types, viewType, true) {
|
if !util.SliceContainsString(types, viewType, true) {
|
||||||
viewType = "all"
|
viewType = "all"
|
||||||
}
|
}
|
||||||
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
|
|
||||||
assigneeID := ctx.FormInt64("assignee")
|
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||||
posterUsername := ctx.FormString("poster")
|
posterUsername := ctx.FormString("poster")
|
||||||
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
||||||
var mentionedID, reviewRequestedID, reviewedID int64
|
var mentionedID, reviewRequestedID, reviewedID int64
|
||||||
@ -512,7 +496,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
switch viewType {
|
switch viewType {
|
||||||
case "created_by":
|
case "created_by":
|
||||||
posterUserID = ctx.Doer.ID
|
posterUserID = optional.Some(ctx.Doer.ID)
|
||||||
case "mentioned":
|
case "mentioned":
|
||||||
mentionedID = ctx.Doer.ID
|
mentionedID = ctx.Doer.ID
|
||||||
case "assigned":
|
case "assigned":
|
||||||
@ -525,18 +509,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
repo := ctx.Repo.Repository
|
||||||
var labelIDs []int64
|
|
||||||
// 1,-2 means including label 1 and excluding label 2
|
|
||||||
// 0 means issues with no label
|
|
||||||
// blank means labels will not be filtered for issues
|
|
||||||
selectLabels := ctx.FormString("labels")
|
|
||||||
if selectLabels != "" {
|
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
|
||||||
if err != nil {
|
|
||||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyword := strings.Trim(ctx.FormString("q"), " ")
|
keyword := strings.Trim(ctx.FormString("q"), " ")
|
||||||
if bytes.Contains([]byte(keyword), []byte{0x00}) {
|
if bytes.Contains([]byte(keyword), []byte{0x00}) {
|
||||||
keyword = ""
|
keyword = ""
|
||||||
@ -547,13 +519,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
mileIDs = []int64{milestoneID}
|
mileIDs = []int64{milestoneID}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var issueStats *issues_model.IssueStats
|
var issueStats *issues_model.IssueStats
|
||||||
statsOpts := &issues_model.IssuesOptions{
|
statsOpts := &issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: optional.Some(assigneeID),
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
@ -634,7 +611,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: optional.Some(assigneeID),
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
@ -709,49 +686,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if repo.Owner.IsOrganization() {
|
|
||||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetLabelsByOrgID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["OrgLabels"] = orgLabels
|
|
||||||
labels = append(labels, orgLabels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the exclusive scope for every label ID
|
|
||||||
labelExclusiveScopes := make([]string, 0, len(labelIDs))
|
|
||||||
for _, labelID := range labelIDs {
|
|
||||||
foundExclusiveScope := false
|
|
||||||
for _, label := range labels {
|
|
||||||
if label.ID == labelID || label.ID == -labelID {
|
|
||||||
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
|
||||||
foundExclusiveScope = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundExclusiveScope {
|
|
||||||
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range labels {
|
|
||||||
l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
|
|
||||||
}
|
|
||||||
ctx.Data["Labels"] = labels
|
|
||||||
ctx.Data["NumLabels"] = len(labels)
|
|
||||||
|
|
||||||
if ctx.FormInt64("assignee") == 0 {
|
|
||||||
assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
|
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
|
||||||
|
|
||||||
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
|
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
|
||||||
@ -792,13 +726,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
ctx.Data["OpenCount"] = issueStats.OpenCount
|
ctx.Data["OpenCount"] = issueStats.OpenCount
|
||||||
ctx.Data["ClosedCount"] = issueStats.ClosedCount
|
ctx.Data["ClosedCount"] = issueStats.ClosedCount
|
||||||
ctx.Data["SelLabelIDs"] = labelIDs
|
ctx.Data["SelLabelIDs"] = labelIDs
|
||||||
ctx.Data["SelectLabels"] = selectLabels
|
|
||||||
ctx.Data["ViewType"] = viewType
|
ctx.Data["ViewType"] = viewType
|
||||||
ctx.Data["SortType"] = sortType
|
ctx.Data["SortType"] = sortType
|
||||||
ctx.Data["MilestoneID"] = milestoneID
|
ctx.Data["MilestoneID"] = milestoneID
|
||||||
ctx.Data["ProjectID"] = projectID
|
ctx.Data["ProjectID"] = projectID
|
||||||
ctx.Data["AssigneeID"] = assigneeID
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
ctx.Data["PosterUserID"] = posterUserID
|
|
||||||
ctx.Data["PosterUsername"] = posterUsername
|
ctx.Data["PosterUsername"] = posterUsername
|
||||||
ctx.Data["Keyword"] = keyword
|
ctx.Data["Keyword"] = keyword
|
||||||
ctx.Data["IsShowClosed"] = isShowClosed
|
ctx.Data["IsShowClosed"] = isShowClosed
|
||||||
@ -810,19 +742,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
|||||||
default:
|
default:
|
||||||
ctx.Data["State"] = "open"
|
ctx.Data["State"] = "open"
|
||||||
}
|
}
|
||||||
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
pager.AddParamString("q", keyword)
|
|
||||||
pager.AddParamString("type", viewType)
|
|
||||||
pager.AddParamString("sort", sortType)
|
|
||||||
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
|
|
||||||
pager.AddParamString("labels", fmt.Sprint(selectLabels))
|
|
||||||
pager.AddParamString("milestone", fmt.Sprint(milestoneID))
|
|
||||||
pager.AddParamString("project", fmt.Sprint(projectID))
|
|
||||||
pager.AddParamString("assignee", fmt.Sprint(assigneeID))
|
|
||||||
pager.AddParamString("poster", posterUsername)
|
|
||||||
if showArchivedLabels {
|
|
||||||
pager.AddParamString("archived_labels", "true")
|
|
||||||
}
|
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
@ -307,23 +308,13 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelIDs []int64
|
labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
||||||
// 1,-2 means including label 1 and excluding label 2
|
|
||||||
// 0 means issues with no label
|
|
||||||
// blank means labels will not be filtered for issues
|
|
||||||
selectLabels := ctx.FormString("labels")
|
|
||||||
if selectLabels != "" {
|
|
||||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
|
||||||
if err != nil {
|
|
||||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assigneeID := ctx.FormInt64("assignee")
|
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: optional.Some(assigneeID),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
@ -409,8 +400,6 @@ func ViewProject(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
ctx.Data["SelectLabels"] = selectLabels
|
|
||||||
ctx.Data["AssigneeID"] = assigneeID
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
|
|
||||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
|
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
|
||||||
|
71
routers/web/shared/issue/issue_label.go
Normal file
71
routers/web/shared/issue/issue_label.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]`
|
||||||
|
func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (labelIDs []int64) {
|
||||||
|
// 1,-2 means including label 1 and excluding label 2
|
||||||
|
// 0 means issues with no label
|
||||||
|
// blank means labels will not be filtered for issues
|
||||||
|
selectLabels := ctx.FormString("labels")
|
||||||
|
if selectLabels != "" {
|
||||||
|
var err error
|
||||||
|
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allLabels []*issues_model.Label
|
||||||
|
if repoID != 0 {
|
||||||
|
repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLabelsByRepoID", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
allLabels = append(allLabels, repoLabels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner != nil && owner.IsOrganization() {
|
||||||
|
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLabelsByOrgID", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
allLabels = append(allLabels, orgLabels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the exclusive scope for every label ID
|
||||||
|
labelExclusiveScopes := make([]string, 0, len(labelIDs))
|
||||||
|
for _, labelID := range labelIDs {
|
||||||
|
foundExclusiveScope := false
|
||||||
|
for _, label := range allLabels {
|
||||||
|
if label.ID == labelID || label.ID == -labelID {
|
||||||
|
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
||||||
|
foundExclusiveScope = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundExclusiveScope {
|
||||||
|
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range allLabels {
|
||||||
|
l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
|
||||||
|
}
|
||||||
|
ctx.Data["Labels"] = allLabels
|
||||||
|
ctx.Data["SelectLabels"] = selectLabels
|
||||||
|
return labelIDs
|
||||||
|
}
|
@ -8,7 +8,9 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/user"
|
"code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/optional"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||||
@ -31,17 +33,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
|||||||
// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list.
|
// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list.
|
||||||
// So it's better to make it work like GitHub: users could input username directly.
|
// So it's better to make it work like GitHub: users could input username directly.
|
||||||
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
|
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
|
||||||
// Old usage: poster=123, new usage: poster=the-username (at the moment, non-existing username is treated as poster=0, not ideal but acceptable)
|
// Return values:
|
||||||
func GetFilterUserIDByName(ctx context.Context, name string) int64 {
|
// * nil: no filter
|
||||||
|
// * some(id): match the id, the id could be -1 to match the issues without assignee
|
||||||
|
// * some(NonExistingID): match no issue (due to the user doesn't exist)
|
||||||
|
func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return 0
|
return optional.None[int64]()
|
||||||
}
|
}
|
||||||
u, err := user.GetUserByName(ctx, name)
|
u, err := user.GetUserByName(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
|
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
|
||||||
return id
|
return optional.Some(id)
|
||||||
}
|
}
|
||||||
return 0
|
return optional.Some(db.NonExistingID)
|
||||||
}
|
}
|
||||||
return u.ID
|
return optional.Some(u.ID)
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/routers/web/feed"
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||||
"code.gitea.io/gitea/routers/web/shared/user"
|
"code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
feed_service "code.gitea.io/gitea/services/feed"
|
feed_service "code.gitea.io/gitea/services/feed"
|
||||||
@ -413,6 +414,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
viewType = "your_repositories"
|
viewType = "your_repositories"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPullList := unitType == unit.TypePullRequests
|
||||||
|
opts := &issues_model.IssuesOptions{
|
||||||
|
IsPull: optional.Some(isPullList),
|
||||||
|
SortType: sortType,
|
||||||
|
IsArchived: optional.Some(false),
|
||||||
|
User: ctx.Doer,
|
||||||
|
}
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Build opts (IssuesOptions), which contains filter information.
|
// Build opts (IssuesOptions), which contains filter information.
|
||||||
// Will eventually be used to retrieve issues relevant for the overview page.
|
// Will eventually be used to retrieve issues relevant for the overview page.
|
||||||
@ -422,30 +430,24 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
// Get repository IDs where User/Org/Team has access.
|
// Get repository IDs where User/Org/Team has access.
|
||||||
var team *organization.Team
|
if ctx.Org != nil && ctx.Org.Organization != nil {
|
||||||
var org *organization.Organization
|
opts.Org = ctx.Org.Organization
|
||||||
if ctx.Org != nil {
|
opts.Team = ctx.Org.Team
|
||||||
org = ctx.Org.Organization
|
|
||||||
team = ctx.Org.Team
|
|
||||||
}
|
|
||||||
|
|
||||||
isPullList := unitType == unit.TypePullRequests
|
issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
|
||||||
opts := &issues_model.IssuesOptions{
|
if ctx.Written() {
|
||||||
IsPull: optional.Some(isPullList),
|
return
|
||||||
SortType: sortType,
|
}
|
||||||
IsArchived: optional.Some(false),
|
|
||||||
Org: org,
|
|
||||||
Team: team,
|
|
||||||
User: ctx.Doer,
|
|
||||||
}
|
}
|
||||||
// Get filter by author id & assignee id
|
// Get filter by author id & assignee id
|
||||||
// FIXME: this feature doesn't work at the moment, because frontend can't use a "user-remote-search" dropdown directly
|
|
||||||
// the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly.
|
// the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly.
|
||||||
// In the future, we need something like github: "author:user1" to accept usernames directly.
|
// In the future, we need something like github: "author:user1" to accept usernames directly.
|
||||||
posterUsername := ctx.FormString("poster")
|
posterUsername := ctx.FormString("poster")
|
||||||
|
ctx.Data["FilterPosterUsername"] = posterUsername
|
||||||
opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername)
|
opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername)
|
||||||
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
|
assigneeUsername := ctx.FormString("assignee")
|
||||||
opts.AssigneeID, _ = strconv.ParseInt(ctx.FormString("assignee"), 10, 64)
|
ctx.Data["FilterAssigneeUsername"] = assigneeUsername
|
||||||
|
opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername)
|
||||||
|
|
||||||
isFuzzy := ctx.FormBool("fuzzy")
|
isFuzzy := ctx.FormBool("fuzzy")
|
||||||
|
|
||||||
@ -471,8 +473,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
UnitType: unitType,
|
UnitType: unitType,
|
||||||
Archived: optional.Some(false),
|
Archived: optional.Some(false),
|
||||||
}
|
}
|
||||||
if team != nil {
|
if opts.Team != nil {
|
||||||
repoOpts.TeamID = team.ID
|
repoOpts.TeamID = opts.Team.ID
|
||||||
}
|
}
|
||||||
accessibleRepos := container.Set[int64]{}
|
accessibleRepos := container.Set[int64]{}
|
||||||
{
|
{
|
||||||
@ -500,9 +502,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
case issues_model.FilterModeAll:
|
case issues_model.FilterModeAll:
|
||||||
case issues_model.FilterModeYourRepositories:
|
case issues_model.FilterModeYourRepositories:
|
||||||
case issues_model.FilterModeAssign:
|
case issues_model.FilterModeAssign:
|
||||||
opts.AssigneeID = ctx.Doer.ID
|
opts.AssigneeID = optional.Some(ctx.Doer.ID)
|
||||||
case issues_model.FilterModeCreate:
|
case issues_model.FilterModeCreate:
|
||||||
opts.PosterID = ctx.Doer.ID
|
opts.PosterID = optional.Some(ctx.Doer.ID)
|
||||||
case issues_model.FilterModeMention:
|
case issues_model.FilterModeMention:
|
||||||
opts.MentionedID = ctx.Doer.ID
|
opts.MentionedID = ctx.Doer.ID
|
||||||
case issues_model.FilterModeReviewRequested:
|
case issues_model.FilterModeReviewRequested:
|
||||||
@ -584,10 +586,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
// because the doer may create issues or be mentioned in any public repo.
|
// because the doer may create issues or be mentioned in any public repo.
|
||||||
// So we need search issues in all public repos.
|
// So we need search issues in all public repos.
|
||||||
o.AllPublic = ctx.Doer.ID == ctxUser.ID
|
o.AllPublic = ctx.Doer.ID == ctxUser.ID
|
||||||
// TODO: to make it work with poster/assignee filter, then these IDs should be kept
|
|
||||||
o.AssigneeID = nil
|
|
||||||
o.PosterID = nil
|
|
||||||
|
|
||||||
o.MentionID = nil
|
o.MentionID = nil
|
||||||
o.ReviewRequestedID = nil
|
o.ReviewRequestedID = nil
|
||||||
o.ReviewedID = nil
|
o.ReviewedID = nil
|
||||||
@ -645,10 +643,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
ctx.Data["ViewType"] = viewType
|
ctx.Data["ViewType"] = viewType
|
||||||
ctx.Data["SortType"] = sortType
|
ctx.Data["SortType"] = sortType
|
||||||
ctx.Data["IsShowClosed"] = isShowClosed
|
ctx.Data["IsShowClosed"] = isShowClosed
|
||||||
ctx.Data["SelectLabels"] = selectedLabels
|
|
||||||
ctx.Data["IsFuzzy"] = isFuzzy
|
ctx.Data["IsFuzzy"] = isFuzzy
|
||||||
ctx.Data["SearchFilterPosterID"] = util.Iif[any](opts.PosterID != 0, opts.PosterID, nil)
|
|
||||||
ctx.Data["SearchFilterAssigneeID"] = util.Iif[any](opts.AssigneeID != 0, opts.AssigneeID, nil)
|
|
||||||
|
|
||||||
if isShowClosed {
|
if isShowClosed {
|
||||||
ctx.Data["State"] = "closed"
|
ctx.Data["State"] = "closed"
|
||||||
@ -657,16 +652,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
|
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
|
||||||
pager.AddParamString("q", keyword)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
pager.AddParamString("type", viewType)
|
|
||||||
pager.AddParamString("sort", sortType)
|
|
||||||
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
|
|
||||||
pager.AddParamString("labels", selectedLabels)
|
|
||||||
pager.AddParamString("fuzzy", fmt.Sprint(isFuzzy))
|
|
||||||
pager.AddParamString("poster", posterUsername)
|
|
||||||
if opts.AssigneeID != 0 {
|
|
||||||
pager.AddParamString("assignee", fmt.Sprint(opts.AssigneeID))
|
|
||||||
}
|
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssues)
|
ctx.HTML(http.StatusOK, tplIssues)
|
||||||
|
@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) {
|
|||||||
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
|
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
|
||||||
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
|
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
|
||||||
})
|
})
|
||||||
m.Get("/search", repo.ListIssues)
|
m.Get("/search", repo.SearchRepoIssuesJSON)
|
||||||
}, context.RepoMustNotBeArchived(), reqRepoIssueReader)
|
}, context.RepoMustNotBeArchived(), reqRepoIssueReader)
|
||||||
|
|
||||||
// FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
|
// FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
|
||||||
|
@ -6,6 +6,7 @@ package context
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -32,6 +33,18 @@ func (p *Pagination) AddParamString(key, value string) {
|
|||||||
p.urlParams = append(p.urlParams, urlParam)
|
p.urlParams = append(p.urlParams, urlParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||||
|
for key, values := range req.URL.Query() {
|
||||||
|
if key == "page" || len(values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value))
|
||||||
|
p.urlParams = append(p.urlParams, urlParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetParams returns the configured URL params
|
// GetParams returns the configured URL params
|
||||||
func (p *Pagination) GetParams() template.URL {
|
func (p *Pagination) GetParams() template.URL {
|
||||||
return template.URL(strings.Join(p.urlParams, "&"))
|
return template.URL(strings.Join(p.urlParams, "&"))
|
||||||
|
@ -24,16 +24,19 @@
|
|||||||
<input type="hidden" name="state" value="{{$.State}}">
|
<input type="hidden" name="state" value="{{$.State}}">
|
||||||
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}}
|
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}}
|
||||||
</form>
|
</form>
|
||||||
<!-- Sort -->
|
|
||||||
<div class="list-header-sort ui small dropdown type jump item">
|
<div class="list-header-filters">
|
||||||
<span class="text">
|
<!-- Sort -->
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
<div class="item ui small dropdown jump">
|
||||||
</span>
|
<span class="text">
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
<div class="menu">
|
</span>
|
||||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
<div class="menu">
|
||||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||||
|
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||||
|
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<span data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>{{svg "octicon-info"}}</span>
|
<span data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>{{svg "octicon-info"}}</span>
|
||||||
</label>
|
</label>
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
<span class="label-filter-exclude-info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
* QueryParamKey: eg: "poster", "assignee"
|
* QueryParamKey: eg: "poster", "assignee"
|
||||||
* QueryLink
|
* QueryLink
|
||||||
* UserSearchUrl
|
* UserSearchUrl
|
||||||
* SelectedUserId
|
* SelectedUsername
|
||||||
* TextFilterTitle
|
* TextFilterTitle
|
||||||
*/}}
|
*/}}
|
||||||
{{$queryLink := .QueryLink}}
|
{{$queryLink := .QueryLink}}
|
||||||
<div class="item ui dropdown custom user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}"
|
<div class="item ui dropdown custom user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}"
|
||||||
data-search-url="{{$.UserSearchUrl}}"
|
data-search-url="{{$.UserSearchUrl}}"
|
||||||
data-selected-user-id="{{$.SelectedUserId}}"
|
data-selected-username="{{$.SelectedUsername}}"
|
||||||
data-action-jump-url="{{QueryBuild $queryLink $.QueryParamKey NIL}}&{{$.QueryParamKey}}={username}"
|
data-action-jump-url="{{QueryBuild $queryLink $.QueryParamKey NIL}}&{{$.QueryParamKey}}={username}"
|
||||||
>
|
>
|
||||||
{{$.TextFilterTitle}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{$.TextFilterTitle}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{{if not .Milestone}}
|
{{if not .Milestone}}
|
||||||
<!-- Milestone -->
|
<!-- Milestone -->
|
||||||
<div class="ui {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}} dropdown jump item">
|
<div class="item ui dropdown jump {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}}">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
|
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
|
||||||
</span>
|
</span>
|
||||||
@ -42,7 +42,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Project -->
|
<!-- Project -->
|
||||||
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
|
<div class="item ui dropdown jump {{if not (or .OpenProjects .ClosedProjects)}}disabled{{end}}">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_project"}}
|
{{ctx.Locale.Tr "repo.issues.filter_project"}}
|
||||||
</span>
|
</span>
|
||||||
@ -84,7 +84,7 @@
|
|||||||
"QueryParamKey" "poster"
|
"QueryParamKey" "poster"
|
||||||
"QueryLink" $queryLink
|
"QueryLink" $queryLink
|
||||||
"UserSearchUrl" (Iif .Milestone (print $.RepoLink "/issues/posters") (print $.Link "/posters"))
|
"UserSearchUrl" (Iif .Milestone (print $.RepoLink "/issues/posters") (print $.Link "/posters"))
|
||||||
"SelectedUserId" $.PosterUserID
|
"SelectedUsername" $.PosterUsername
|
||||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster")
|
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster")
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
{{if .IsSigned}}
|
{{if .IsSigned}}
|
||||||
<!-- Type -->
|
<!-- Type -->
|
||||||
<div class="ui dropdown type jump item">
|
<div class="item ui dropdown jump">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type"}}
|
||||||
</span>
|
</span>
|
||||||
@ -119,7 +119,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Sort -->
|
<!-- Sort -->
|
||||||
<div class="list-header-sort ui small dropdown downward type jump item">
|
<div class="item ui dropdown jump">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!-- Sort -->
|
<!-- Sort -->
|
||||||
<div class="list-header-sort ui small dropdown type jump item">
|
<div class="item ui small dropdown jump">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
</span>
|
</span>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<input type="hidden" name="state" value="{{$.State}}">
|
<input type="hidden" name="state" value="{{$.State}}">
|
||||||
{{if not .PageIsMilestones}}
|
{{if not .PageIsMilestones}}
|
||||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||||
<input type="hidden" name="labels" value="{{.SelectLabels}}">
|
<input type="hidden" name="labels" value="{{$.SelectLabels}}">
|
||||||
<input type="hidden" name="milestone" value="{{$.MilestoneID}}">
|
<input type="hidden" name="milestone" value="{{$.MilestoneID}}">
|
||||||
<input type="hidden" name="project" value="{{$.ProjectID}}">
|
<input type="hidden" name="project" value="{{$.ProjectID}}">
|
||||||
<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
|
<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
{{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "fuzzy" $.IsFuzzy}}
|
{{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "labels" .SelectLabels "fuzzy" $.IsFuzzy}}
|
||||||
<div class="flex-container-nav">
|
<div class="flex-container-nav">
|
||||||
<div class="ui secondary vertical filter menu tw-bg-transparent">
|
<div class="ui secondary vertical filter menu tw-bg-transparent">
|
||||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
|
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
|
||||||
@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.SearchFilterPosterUsername "assignee" $.SearchFilterAssigneeID}}
|
{{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.FilterPosterUsername "assignee" $.FilterAssigneeUsername}}
|
||||||
<div class="flex-container-main content">
|
<div class="flex-container-main content">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<div class="small-menu-items ui compact tiny menu list-header-toggle flex-items-block">
|
<div class="small-menu-items ui compact tiny menu list-header-toggle flex-items-block">
|
||||||
@ -50,28 +50,51 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<form class="list-header-search ui form ignore-dirty">
|
<form class="list-header-search ui form ignore-dirty">
|
||||||
<div class="ui small search fluid action input">
|
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
<input type="hidden" name="sort" value="{{$.SortType}}">
|
||||||
<input type="hidden" name="sort" value="{{$.SortType}}">
|
<input type="hidden" name="state" value="{{$.State}}">
|
||||||
<input type="hidden" name="state" value="{{$.State}}">
|
{{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr (Iif .PageIsPulls "search.pull_kind" "search.issue_kind")) "Tooltip" (ctx.Locale.Tr "explore.go_to")}}
|
||||||
{{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr (Iif .PageIsPulls "search.pull_kind" "search.issue_kind")) "Tooltip" (ctx.Locale.Tr "explore.go_to")}}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<!-- Sort -->
|
|
||||||
<div class="list-header-sort ui small dropdown type jump item">
|
<div class="list-header-filters">
|
||||||
<span class="text tw-whitespace-nowrap">
|
{{if $.Labels}}
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLinkWithFilter "SupportArchivedLabel" true}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{end}}
|
||||||
</span>
|
|
||||||
<div class="menu">
|
{{if ne $.ViewType "created_by"}}
|
||||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
{{template "repo/issue/filter_item_user_fetch" dict
|
||||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
"QueryParamKey" "poster"
|
||||||
<a class="{{if eq .SortType "latest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
"QueryLink" $queryLinkWithFilter
|
||||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
"SelectedUsername" $.FilterPosterUsername
|
||||||
<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster")
|
||||||
<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
}}
|
||||||
<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
|
{{end}}
|
||||||
<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
|
|
||||||
|
{{if ne $.ViewType "assigned"}}
|
||||||
|
{{template "repo/issue/filter_item_user_fetch" dict
|
||||||
|
"QueryParamKey" "assignee"
|
||||||
|
"QueryLink" $queryLinkWithFilter
|
||||||
|
"SelectedUsername" $.FilterAssigneeUsername
|
||||||
|
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
||||||
|
}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Sort -->
|
||||||
|
<div class="item ui small dropdown jump">
|
||||||
|
<span class="text tw-whitespace-nowrap">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||||
|
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||||
|
<a class="{{if eq .SortType "latest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||||
|
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||||
|
<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
||||||
|
<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
||||||
|
<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
|
||||||
|
<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,20 +52,22 @@
|
|||||||
<input type="hidden" name="state" value="{{$.State}}">
|
<input type="hidden" name="state" value="{{$.State}}">
|
||||||
{{template "shared/search/combo" dict "Value" $.Keyword}}
|
{{template "shared/search/combo" dict "Value" $.Keyword}}
|
||||||
</form>
|
</form>
|
||||||
<!-- Sort -->
|
<div class="list-header-filters">
|
||||||
<div class="list-header-sort ui dropdown type jump item">
|
<!-- Sort -->
|
||||||
<span class="text">
|
<div class="item ui dropdown jump">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
<span class="text">
|
||||||
</span>
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
</span>
|
||||||
<div class="menu">
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
|
<div class="menu">
|
||||||
<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
|
<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
|
||||||
<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
|
<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
|
||||||
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
|
||||||
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||||
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||||
|
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,24 +90,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .filter.menu .ui.dropdown.label-filter .menu .info {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .filter.menu .ui.dropdown.label-filter .menu .info code {
|
|
||||||
border: 1px solid var(--color-secondary);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 1px 2px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For the secondary pointing menu, respect its own border-bottom */
|
/* For the secondary pointing menu, respect its own border-bottom */
|
||||||
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
||||||
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
||||||
|
@ -73,3 +73,21 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-filter-exclude-info {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-filter-exclude-info code {
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 1px 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
@ -5,13 +5,6 @@
|
|||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header-sort {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header-search {
|
.list-header-search {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -21,8 +14,22 @@
|
|||||||
min-width: 200px; /* to enable flexbox wrapping on mobile */
|
min-width: 200px; /* to enable flexbox wrapping on mobile */
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header-search .input {
|
.list-header-search > .ui.input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header-search > .ui.input .ui.dropdown {
|
||||||
|
min-width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header-filters > .item {
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
@ -32,8 +39,7 @@
|
|||||||
.list-header-toggle {
|
.list-header-toggle {
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
.list-header-sort {
|
.list-header-filters {
|
||||||
order: 2;
|
order: 2;
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,10 +95,9 @@ function initRepoIssueListCheckboxes() {
|
|||||||
function initDropdownUserRemoteSearch(el: Element) {
|
function initDropdownUserRemoteSearch(el: Element) {
|
||||||
let searchUrl = el.getAttribute('data-search-url');
|
let searchUrl = el.getAttribute('data-search-url');
|
||||||
const actionJumpUrl = el.getAttribute('data-action-jump-url');
|
const actionJumpUrl = el.getAttribute('data-action-jump-url');
|
||||||
const selectedUserId = parseInt(el.getAttribute('data-selected-user-id'));
|
let selectedUsername = el.getAttribute('data-selected-username') || '';
|
||||||
let selectedUsername = '';
|
|
||||||
if (!searchUrl.includes('?')) searchUrl += '?';
|
|
||||||
const $searchDropdown = fomanticQuery(el);
|
const $searchDropdown = fomanticQuery(el);
|
||||||
|
const elMenu = el.querySelector('.menu');
|
||||||
const elSearchInput = el.querySelector<HTMLInputElement>('.ui.search input');
|
const elSearchInput = el.querySelector<HTMLInputElement>('.ui.search input');
|
||||||
const elItemFromInput = el.querySelector('.menu > .item-from-input');
|
const elItemFromInput = el.querySelector('.menu > .item-from-input');
|
||||||
|
|
||||||
@ -110,17 +109,27 @@ function initDropdownUserRemoteSearch(el: Element) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectUsername = (username: string) => {
|
||||||
|
queryElems(elMenu, '.item.active, .item.selected', (el) => el.classList.remove('active', 'selected'));
|
||||||
|
elMenu.querySelector(`.item[data-value="${CSS.escape(username)}"]`)?.classList.add('selected');
|
||||||
|
};
|
||||||
|
|
||||||
type ProcessedResult = {value: string, name: string};
|
type ProcessedResult = {value: string, name: string};
|
||||||
const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items
|
const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items
|
||||||
const syncItemFromInput = () => {
|
const syncItemFromInput = () => {
|
||||||
elItemFromInput.setAttribute('data-value', elSearchInput.value);
|
const inputVal = elSearchInput.value.trim();
|
||||||
elItemFromInput.textContent = elSearchInput.value;
|
elItemFromInput.setAttribute('data-value', inputVal);
|
||||||
toggleElem(elItemFromInput, !processedResults.length);
|
elItemFromInput.textContent = inputVal;
|
||||||
|
const showItemFromInput = !processedResults.length && inputVal !== '';
|
||||||
|
toggleElem(elItemFromInput, showItemFromInput);
|
||||||
|
selectUsername(showItemFromInput ? inputVal : selectedUsername);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
elSearchInput.value = selectedUsername;
|
||||||
if (!searchUrl) {
|
if (!searchUrl) {
|
||||||
elSearchInput.addEventListener('input', syncItemFromInput);
|
elSearchInput.addEventListener('input', syncItemFromInput);
|
||||||
} else {
|
} else {
|
||||||
|
if (!searchUrl.includes('?')) searchUrl += '?';
|
||||||
$searchDropdown.dropdown('setting', 'apiSettings', {
|
$searchDropdown.dropdown('setting', 'apiSettings', {
|
||||||
cache: false,
|
cache: false,
|
||||||
url: `${searchUrl}&q={query}`,
|
url: `${searchUrl}&q={query}`,
|
||||||
@ -130,11 +139,10 @@ function initDropdownUserRemoteSearch(el: Element) {
|
|||||||
for (const item of resp.results) {
|
for (const item of resp.results) {
|
||||||
let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
|
let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
|
||||||
if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
|
if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
|
||||||
if (selectedUserId === item.user_id) selectedUsername = item.username;
|
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
||||||
processedResults.push({value: item.username, name: html});
|
processedResults.push({value: item.username, name: html});
|
||||||
}
|
}
|
||||||
resp.results = processedResults;
|
resp.results = processedResults;
|
||||||
syncItemFromInput();
|
|
||||||
return resp;
|
return resp;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -146,9 +154,8 @@ function initDropdownUserRemoteSearch(el: Element) {
|
|||||||
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
|
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
|
||||||
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
|
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
|
||||||
dropdownSetup.menu = function (values) {
|
dropdownSetup.menu = function (values) {
|
||||||
const menu = $searchDropdown.find('> .menu')[0];
|
|
||||||
// remove old dynamic items
|
// remove old dynamic items
|
||||||
for (const el of menu.querySelectorAll(':scope > .dynamic-item')) {
|
for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) {
|
||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,16 +167,11 @@ function initDropdownUserRemoteSearch(el: Element) {
|
|||||||
}
|
}
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.classList.add('divider', 'dynamic-item');
|
div.classList.add('divider', 'dynamic-item');
|
||||||
menu.append(div, ...newMenuItems);
|
elMenu.append(div, ...newMenuItems);
|
||||||
}
|
}
|
||||||
$searchDropdown.dropdown('refresh');
|
$searchDropdown.dropdown('refresh');
|
||||||
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
|
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
|
||||||
setTimeout(() => {
|
setTimeout(() => syncItemFromInput(), 0);
|
||||||
for (const el of menu.querySelectorAll('.item.active, .item.selected')) {
|
|
||||||
el.classList.remove('active', 'selected');
|
|
||||||
}
|
|
||||||
menu.querySelector(`.item[data-value="${CSS.escape(selectedUsername)}"]`)?.classList.add('selected');
|
|
||||||
}, 0);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,8 +223,12 @@ async function initIssuePinSort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoIssueList() {
|
export function initRepoIssueList() {
|
||||||
if (!document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) return;
|
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
||||||
initRepoIssueListCheckboxes();
|
initRepoIssueListCheckboxes();
|
||||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||||
initIssuePinSort();
|
initIssuePinSort();
|
||||||
|
} else if (document.querySelector('.page-content.dashboard.issues')) {
|
||||||
|
// user or org home: issue list, pull request list
|
||||||
|
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user