mirror of
https://github.com/go-gitea/gitea
synced 2024-12-23 00:54:28 +00:00
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 ![image](https://github.com/user-attachments/assets/1837ace8-3de2-444f-a153-e166bd0da2c0) **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 URL
This commit is contained in:
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)
|
||||||
}
|
}
|
||||||
|
22
routers/web/shared/user/helper.go
Normal file
22
routers/web/shared/user/helper.go
Normal file
@ -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
Block a user