mirror of https://github.com/go-gitea/gitea.git
Add option to filter board cards by labels and assignees (#31999)
Works in both organization and repository project boards Fixes #21846 Replaces #21963 Replaces #27117  **Note** that implementation was made intentionally to work same as in issue list so that URL can be bookmarked for quick access with predefined filters in URLpull/31841/head^2
parent
20d7707124
commit
4ab6fc62d2
|
@ -48,12 +48,12 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIssuesFromColumn load issues assigned to this column
|
// LoadIssuesFromColumn load issues assigned to this column
|
||||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
|
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
|
||||||
issueList, err := Issues(ctx, &IssuesOptions{
|
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||||
ProjectColumnID: b.ID,
|
o.ProjectColumnID = b.ID
|
||||||
ProjectID: b.ProjectID,
|
o.ProjectID = b.ProjectID
|
||||||
SortType: "project-column-sorting",
|
o.SortType = "project-column-sorting"
|
||||||
})
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,10 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueLi
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIssuesFromColumnList load issues assigned to the columns
|
// LoadIssuesFromColumnList load issues assigned to the columns
|
||||||
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
|
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
|
||||||
issuesMap := make(map[int64]IssueList, len(bs))
|
issuesMap := make(map[int64]IssueList, len(bs))
|
||||||
for i := range bs {
|
for i := range bs {
|
||||||
il, err := LoadIssuesFromColumn(ctx, bs[i])
|
il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,19 @@ type IssuesOptions struct { //nolint
|
||||||
User *user_model.User // issues permission scope
|
User *user_model.User // issues permission scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of the options.
|
||||||
|
// Be careful, it's not a deep copy, so `IssuesOptions.RepoIDs = {...}` is OK while `IssuesOptions.RepoIDs[0] = ...` is not.
|
||||||
|
func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOptions {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := *o
|
||||||
|
for _, e := range edit {
|
||||||
|
e(&v)
|
||||||
|
}
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
// applySorts sort an issues-related session based on the provided
|
// applySorts sort an issues-related session based on the provided
|
||||||
// sortType string
|
// sortType string
|
||||||
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
"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/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
@ -112,6 +114,49 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOrgAssignees returns all users that have write access and can be assigned to issues
|
||||||
|
// of the any repository in the organization.
|
||||||
|
func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) {
|
||||||
|
e := db.GetEngine(ctx)
|
||||||
|
userIDs := make([]int64, 0, 10)
|
||||||
|
if err = e.Table("access").
|
||||||
|
Join("INNER", "repository", "`repository`.id = `access`.repo_id").
|
||||||
|
Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite).
|
||||||
|
Select("user_id").
|
||||||
|
Find(&userIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalUserIDs := make([]int64, 0, 10)
|
||||||
|
if err = e.Table("team_user").
|
||||||
|
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||||
|
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||||
|
Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id").
|
||||||
|
Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
||||||
|
orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
||||||
|
Distinct("`team_user`.uid").
|
||||||
|
Select("`team_user`.uid").
|
||||||
|
Find(&additionalUserIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueUserIDs := make(container.Set[int64])
|
||||||
|
uniqueUserIDs.AddMultiple(userIDs...)
|
||||||
|
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||||
|
|
||||||
|
users := make([]*user_model.User, 0, len(uniqueUserIDs))
|
||||||
|
if len(userIDs) > 0 {
|
||||||
|
if err = e.In("id", uniqueUserIDs.Values()).
|
||||||
|
Where(builder.Eq{"`user`.is_active": true}).
|
||||||
|
OrderBy(user_model.GetOrderByName()).
|
||||||
|
Find(&users); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
org_model "code.gitea.io/gitea/models/organization"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
attachment_model "code.gitea.io/gitea/models/repo"
|
attachment_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
@ -333,7 +334,29 @@ func ViewProject(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
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 == "" {
|
||||||
|
ctx.Data["AllLabels"] = true
|
||||||
|
} else if selectLabels == "0" {
|
||||||
|
ctx.Data["NoLabel"] = true
|
||||||
|
}
|
||||||
|
if len(selectLabels) > 0 {
|
||||||
|
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assigneeID := ctx.FormInt64("assignee")
|
||||||
|
|
||||||
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||||
|
LabelIDs: labelIDs,
|
||||||
|
AssigneeID: assigneeID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
return
|
return
|
||||||
|
@ -372,6 +395,46 @@ func ViewProject(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add option to filter also by repository specific labels
|
||||||
|
labels, err := issues_model.GetLabelsByOrgID(ctx, project.OwnerID, "", db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLabelsByOrgID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Get assignees.
|
||||||
|
assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
|
ctx.Data["SelectLabels"] = selectLabels
|
||||||
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
|
|
||||||
project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description)
|
project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description)
|
||||||
ctx.Data["LinkedPRs"] = linkedPrsMap
|
ctx.Data["LinkedPRs"] = linkedPrsMap
|
||||||
ctx.Data["PageIsViewProjects"] = true
|
ctx.Data["PageIsViewProjects"] = true
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
"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/repo"
|
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"
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ func List(ctx *context.Context) {
|
||||||
ctx.ServerError("GetActors", err)
|
ctx.ServerError("GetActors", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors)
|
ctx.Data["Actors"] = shared_user.MakeSelfOnTop(ctx.Doer, actors)
|
||||||
|
|
||||||
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx)
|
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx)
|
||||||
|
|
||||||
|
|
|
@ -5,25 +5,11 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/user"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
|
||||||
if doer != nil {
|
|
||||||
sort.Slice(users, func(i, j int) bool {
|
|
||||||
if users[i].ID == users[j].ID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return users[i].ID == doer.ID // if users[i] is self, put it before others, so less=true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleGitError(ctx *context.Context, msg string, err error) {
|
func HandleGitError(ctx *context.Context, msg string, err error) {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
refType := ""
|
refType := ""
|
||||||
|
|
|
@ -49,6 +49,7 @@ import (
|
||||||
"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/utils"
|
"code.gitea.io/gitea/routers/utils"
|
||||||
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
|
@ -360,7 +361,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
handleTeamMentions(ctx)
|
handleTeamMentions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
|
@ -580,7 +581,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
handleTeamMentions(ctx)
|
handleTeamMentions(ctx)
|
||||||
}
|
}
|
||||||
|
@ -3771,7 +3772,7 @@ func issuePosters(ctx *context.Context, isPullList bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posters = MakeSelfOnTop(ctx.Doer, posters)
|
posters = shared_user.MakeSelfOnTop(ctx.Doer, posters)
|
||||||
|
|
||||||
resp := &userSearchResponse{}
|
resp := &userSearchResponse{}
|
||||||
resp.Results = make([]*userSearchInfo, len(posters))
|
resp.Results = make([]*userSearchInfo, len(posters))
|
||||||
|
|
|
@ -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"
|
||||||
|
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"
|
||||||
project_service "code.gitea.io/gitea/services/projects"
|
project_service "code.gitea.io/gitea/services/projects"
|
||||||
|
@ -313,7 +314,29 @@ func ViewProject(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
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 == "" {
|
||||||
|
ctx.Data["AllLabels"] = true
|
||||||
|
} else if selectLabels == "0" {
|
||||||
|
ctx.Data["NoLabel"] = true
|
||||||
|
}
|
||||||
|
if len(selectLabels) > 0 {
|
||||||
|
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assigneeID := ctx.FormInt64("assignee")
|
||||||
|
|
||||||
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||||
|
LabelIDs: labelIDs,
|
||||||
|
AssigneeID: assigneeID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
return
|
return
|
||||||
|
@ -353,6 +376,55 @@ func ViewProject(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
ctx.Data["LinkedPRs"] = linkedPrsMap
|
ctx.Data["LinkedPRs"] = linkedPrsMap
|
||||||
|
|
||||||
|
labels, err := issues_model.GetLabelsByRepoID(ctx, project.RepoID, "", db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLabelsByRepoID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Repo.Owner.IsOrganization() {
|
||||||
|
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, "", db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLabelsByOrgID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Get assignees.
|
||||||
|
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
|
ctx.Data["SelectLabels"] = selectLabels
|
||||||
|
ctx.Data["AssigneeID"] = assigneeID
|
||||||
|
|
||||||
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
||||||
Links: markup.Links{
|
Links: markup.Links{
|
||||||
Base: ctx.Repo.RepoLink,
|
Base: ctx.Repo.RepoLink,
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"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/utils"
|
"code.gitea.io/gitea/routers/utils"
|
||||||
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/automerge"
|
"code.gitea.io/gitea/services/automerge"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
@ -825,7 +826,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
handleTeamMentions(ctx)
|
handleTeamMentions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"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/feed"
|
"code.gitea.io/gitea/routers/web/feed"
|
||||||
|
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/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
@ -370,7 +371,7 @@ func NewRelease(ctx *context.Context) {
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
upload.AddUploadContext(ctx, "release")
|
upload.AddUploadContext(ctx, "release")
|
||||||
|
|
||||||
|
@ -559,7 +560,7 @@ func EditRelease(ctx *context.Context) {
|
||||||
ctx.ServerError("GetRepoAssignees", err)
|
ctx.ServerError("GetRepoAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||||
|
if doer != nil {
|
||||||
|
sort.Slice(users, func(i, j int) bool {
|
||||||
|
if users[i].ID == users[j].ID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return users[i].ID == doer.ID // if users[i] is self, put it before others, so less=true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -3,6 +3,82 @@
|
||||||
<div class="ui container tw-max-w-full">
|
<div class="ui container tw-max-w-full">
|
||||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
|
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
|
||||||
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
|
<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
|
||||||
|
<div class="project-toolbar-right">
|
||||||
|
<div class="ui secondary filter menu labels">
|
||||||
|
<!-- Label -->
|
||||||
|
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item label-filter">
|
||||||
|
<span class="text">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.filter_label"}}
|
||||||
|
</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_label"}}">
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox compact archived-label-filter">
|
||||||
|
<input name="archived" type="checkbox"
|
||||||
|
id="archived-filter-checkbox"
|
||||||
|
{{if .ShowArchivedLabels}}checked{{end}}
|
||||||
|
>
|
||||||
|
<label for="archived-filter-checkbox">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.label_archived_filter"}}
|
||||||
|
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
|
||||||
|
{{svg "octicon-info"}}
|
||||||
|
</i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<a class="{{if .AllLabels}}active selected {{end}}item" href="?assignee={{$.AssigneeID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||||
|
<a class="{{if .NoLabel}}active selected {{end}}item" href="?assignee={{$.AssigneeID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||||
|
{{$previousExclusiveScope := "_no_scope"}}
|
||||||
|
{{range .Labels}}
|
||||||
|
{{$exclusiveScope := .ExclusiveScope}}
|
||||||
|
{{if and (ne $previousExclusiveScope $exclusiveScope)}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{end}}
|
||||||
|
{{$previousExclusiveScope = $exclusiveScope}}
|
||||||
|
<a class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?labels={{.QueryString}}&assignee={{$.AssigneeID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}">
|
||||||
|
{{if .IsExcluded}}
|
||||||
|
{{svg "octicon-circle-slash"}}
|
||||||
|
{{else if .IsSelected}}
|
||||||
|
{{if $exclusiveScope}}
|
||||||
|
{{svg "octicon-dot-fill"}}
|
||||||
|
{{else}}
|
||||||
|
{{svg "octicon-check"}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{RenderLabel $.Context ctx.Locale .}}
|
||||||
|
<p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assignee -->
|
||||||
|
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.filter_assignee"}}
|
||||||
|
</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
<div class="ui icon search input">
|
||||||
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}">
|
||||||
|
</div>
|
||||||
|
<a class="{{if not .AssigneeID}}active selected {{end}}item" href="?labels={{.SelectLabels}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a>
|
||||||
|
<a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?labels={{.SelectLabels}}&assignee=-1{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{range .Assignees}}
|
||||||
|
<a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="?labels={{$.SelectLabels}}&assignee={{.ID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">
|
||||||
|
{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{if $canWriteProject}}
|
{{if $canWriteProject}}
|
||||||
<div class="ui compact mini menu">
|
<div class="ui compact mini menu">
|
||||||
<a class="item" href="{{.Link}}/edit?redirect=project">
|
<a class="item" href="{{.Link}}/edit?redirect=project">
|
||||||
|
|
|
@ -6,6 +6,18 @@
|
||||||
margin: 0 0.5em;
|
margin: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-toolbar-right .filter.menu {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.project-toolbar-right .dropdown .menu {
|
||||||
|
left: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.project-column {
|
.project-column {
|
||||||
background-color: var(--color-project-column-bg) !important;
|
background-color: var(--color-project-column-bg) !important;
|
||||||
border: 1px solid var(--color-secondary) !important;
|
border: 1px solid var(--color-secondary) !important;
|
||||||
|
|
Loading…
Reference in New Issue