mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 07:48:48 +01:00
fix(repo): unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597)
The design of DefaultShowFullName has some problems, which make the UI inconsistent, see the new comment in code This PR does a clean up for various legacy problems, and clarify some "user name display" behaviors. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -147,19 +147,21 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
|
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
|
||||||
// If isShowFullName is set to true, also include full name prefix search
|
// It searches with the "user.name" and "user.full_name" fields case-insensitively.
|
||||||
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
|
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) {
|
||||||
users := make([]*user_model.User, 0, 30)
|
users := make([]*user_model.User, 0, 30)
|
||||||
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
|
|
||||||
if search != "" && isShowFullName {
|
|
||||||
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
|
|
||||||
}
|
|
||||||
|
|
||||||
cond := builder.In("`user`.id",
|
cond := builder.In("`user`.id",
|
||||||
builder.Select("poster_id").From("issue").Where(
|
builder.Select("poster_id").From("issue").Where(
|
||||||
builder.Eq{"repo_id": repo.ID}.
|
builder.Eq{"repo_id": repo.ID}.
|
||||||
And(builder.Eq{"is_pull": isPull}),
|
And(builder.Eq{"is_pull": isPull}),
|
||||||
).GroupBy("poster_id")).And(prefixCond)
|
).GroupBy("poster_id"))
|
||||||
|
|
||||||
|
if search != "" {
|
||||||
|
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
|
||||||
|
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
|
||||||
|
cond = cond.And(prefixCond)
|
||||||
|
}
|
||||||
|
|
||||||
return users, db.GetEngine(ctx).
|
return users, db.GetEngine(ctx).
|
||||||
Where(cond).
|
Where(cond).
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ func TestGetIssuePostersWithSearch(t *testing.T) {
|
|||||||
|
|
||||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
|
||||||
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER", false /* full name */)
|
users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, users, 1)
|
require.Len(t, users, 1)
|
||||||
assert.Equal(t, "user2", users[0].Name)
|
assert.Equal(t, "user2", users[0].Name)
|
||||||
|
|
||||||
users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O", true /* full name */)
|
users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, users, 1)
|
require.Len(t, users, 1)
|
||||||
assert.Equal(t, "user2", users[0].Name)
|
assert.Equal(t, "user2", users[0].Name)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"mime"
|
"mime"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -28,6 +29,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
@@ -417,16 +419,6 @@ func (u *User) IsTokenAccessAllowed() bool {
|
|||||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns full name if it's not empty,
|
|
||||||
// returns username otherwise.
|
|
||||||
func (u *User) DisplayName() string {
|
|
||||||
trimmed := strings.TrimSpace(u.FullName)
|
|
||||||
if len(trimmed) > 0 {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
return u.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
||||||
func (u *User) EmailTo() string {
|
func (u *User) EmailTo() string {
|
||||||
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
|
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
|
||||||
@@ -445,27 +437,45 @@ func (u *User) EmailTo() string {
|
|||||||
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
|
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
|
// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
|
||||||
// returns username otherwise.
|
// * user.Name / user.FullName: directly used in templates
|
||||||
|
// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
|
||||||
|
// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
|
||||||
|
// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles
|
||||||
|
// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy
|
||||||
|
|
||||||
|
// DisplayName returns full name if it's not empty, returns username otherwise.
|
||||||
|
func (u *User) DisplayName() string {
|
||||||
|
fullName := strings.TrimSpace(u.FullName)
|
||||||
|
if fullName != "" {
|
||||||
|
return fullName
|
||||||
|
}
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username.
|
||||||
func (u *User) GetDisplayName() string {
|
func (u *User) GetDisplayName() string {
|
||||||
if setting.UI.DefaultShowFullName {
|
if setting.UI.DefaultShowFullName {
|
||||||
trimmed := strings.TrimSpace(u.FullName)
|
fullName := strings.TrimSpace(u.FullName)
|
||||||
if len(trimmed) > 0 {
|
if fullName != "" {
|
||||||
return trimmed
|
return fullName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return u.Name
|
return u.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompleteName returns the full name and username in the form of
|
// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
|
||||||
// "Full Name (username)" if full name is not empty, otherwise it returns
|
func (u *User) ShortName(length int) string {
|
||||||
// "username".
|
return util.EllipsisDisplayString(u.GetDisplayName(), length)
|
||||||
func (u *User) GetCompleteName() string {
|
}
|
||||||
trimmedFullName := strings.TrimSpace(u.FullName)
|
|
||||||
if len(trimmedFullName) > 0 {
|
func (u *User) GetShortDisplayNameLinkHTML() template.HTML {
|
||||||
return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
|
fullName := strings.TrimSpace(u.FullName)
|
||||||
|
displayName, displayTooltip := u.Name, fullName
|
||||||
|
if setting.UI.DefaultShowFullName && fullName != "" {
|
||||||
|
displayName, displayTooltip = fullName, u.Name
|
||||||
}
|
}
|
||||||
return u.Name
|
return htmlutil.HTMLFormat(`<a class="muted" href="%s" data-tooltip-content="%s">%s</a>`, u.HomeLink(), displayTooltip, displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitSafeName(name string) string {
|
func gitSafeName(name string) string {
|
||||||
@@ -488,14 +498,6 @@ func (u *User) GitName() string {
|
|||||||
return fmt.Sprintf("user-%d", u.ID)
|
return fmt.Sprintf("user-%d", u.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShortName ellipses username to length
|
|
||||||
func (u *User) ShortName(length int) string {
|
|
||||||
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
|
||||||
return util.EllipsisDisplayString(u.FullName, length)
|
|
||||||
}
|
|
||||||
return util.EllipsisDisplayString(u.Name, length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMailable checks if a user is eligible to receive emails.
|
// IsMailable checks if a user is eligible to receive emails.
|
||||||
// System users like Ghost and Gitea Actions are excluded.
|
// System users like Ghost and Gitea Actions are excluded.
|
||||||
func (u *User) IsMailable() bool {
|
func (u *User) IsMailable() bool {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ var UI = struct {
|
|||||||
ReactionMaxUserNum int
|
ReactionMaxUserNum int
|
||||||
MaxDisplayFileSize int64
|
MaxDisplayFileSize int64
|
||||||
ShowUserEmail bool
|
ShowUserEmail bool
|
||||||
DefaultShowFullName bool
|
|
||||||
DefaultTheme string
|
DefaultTheme string
|
||||||
Themes []string
|
Themes []string
|
||||||
FileIconTheme string
|
FileIconTheme string
|
||||||
@@ -43,6 +42,15 @@ var UI = struct {
|
|||||||
|
|
||||||
AmbiguousUnicodeDetection bool
|
AmbiguousUnicodeDetection bool
|
||||||
|
|
||||||
|
// TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710
|
||||||
|
// But there are still many edge cases:
|
||||||
|
// * Many places still use "username", not respecting this setting
|
||||||
|
// * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't
|
||||||
|
// * Even if DefaultShowFullName=false, many places still need to show the full name
|
||||||
|
// For most cases, either "username" or "username (Full Name)" should be used and are good enough.
|
||||||
|
// Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used.
|
||||||
|
DefaultShowFullName bool
|
||||||
|
|
||||||
Notification struct {
|
Notification struct {
|
||||||
MinTimeout time.Duration
|
MinTimeout time.Duration
|
||||||
TimeoutStep time.Duration
|
TimeoutStep time.Duration
|
||||||
|
|||||||
@@ -96,9 +96,6 @@ func NewFuncMap() template.FuncMap {
|
|||||||
"AssetVersion": func() string {
|
"AssetVersion": func() string {
|
||||||
return setting.AssetVersion
|
return setting.AssetVersion
|
||||||
},
|
},
|
||||||
"DefaultShowFullName": func() bool {
|
|
||||||
return setting.UI.DefaultShowFullName
|
|
||||||
},
|
|
||||||
"ShowFooterTemplateLoadTime": func() bool {
|
"ShowFooterTemplateLoadTime": func() bool {
|
||||||
return setting.Other.ShowFooterTemplateLoadTime
|
return setting.Other.ShowFooterTemplateLoadTime
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
gitea_html "code.gitea.io/gitea/modules/htmlutil"
|
gitea_html "code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AvatarUtils struct {
|
type AvatarUtils struct {
|
||||||
@@ -29,13 +30,9 @@ func NewAvatarUtils(ctx context.Context) *AvatarUtils {
|
|||||||
// AvatarHTML creates the HTML for an avatar
|
// AvatarHTML creates the HTML for an avatar
|
||||||
func AvatarHTML(src string, size int, class, name string) template.HTML {
|
func AvatarHTML(src string, size int, class, name string) template.HTML {
|
||||||
sizeStr := strconv.Itoa(size)
|
sizeStr := strconv.Itoa(size)
|
||||||
|
name = util.IfZero(name, "avatar")
|
||||||
if name == "" {
|
|
||||||
name = "avatar"
|
|
||||||
}
|
|
||||||
|
|
||||||
// use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width
|
// use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width
|
||||||
return template.HTML(`<img loading="lazy" alt class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
return template.HTML(`<img loading="lazy" alt class="` + html.EscapeString(class) + `" src="` + html.EscapeString(src) + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `">`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avatar renders user avatars. args: user, size (int), class (string)
|
// Avatar renders user avatars. args: user, size (int), class (string)
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"html"
|
"html"
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/avatars"
|
"code.gitea.io/gitea/models/avatars"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/modules/htmlutil"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
@@ -53,29 +54,25 @@ func GetContentHistoryList(ctx *context.Context) {
|
|||||||
// value is historyId
|
// value is historyId
|
||||||
var results []map[string]any
|
var results []map[string]any
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var actionText string
|
var actionHTML template.HTML
|
||||||
if item.IsDeleted {
|
if item.IsDeleted {
|
||||||
actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
|
actionHTML = htmlutil.HTMLFormat(`<i data-history-is-deleted="1">%s</i>`, ctx.Locale.TrString("repo.issues.content_history.deleted"))
|
||||||
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
|
|
||||||
} else if item.IsFirstCreated {
|
} else if item.IsFirstCreated {
|
||||||
actionText = ctx.Locale.TrString("repo.issues.content_history.created")
|
actionHTML = ctx.Locale.Tr("repo.issues.content_history.created")
|
||||||
} else {
|
} else {
|
||||||
actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
|
actionHTML = ctx.Locale.Tr("repo.issues.content_history.edited")
|
||||||
}
|
}
|
||||||
|
|
||||||
username := item.UserName
|
var fullNameHTML template.HTML
|
||||||
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
|
userName, fullName := item.UserName, strings.TrimSpace(item.UserFullName)
|
||||||
username = strings.TrimSpace(item.UserFullName)
|
if fullName != "" {
|
||||||
|
fullNameHTML = htmlutil.HTMLFormat(` (<span class="tw-inline-flex tw-max-w-[160px]"><span class="gt-ellipsis">%s</span></span>)`, fullName)
|
||||||
}
|
}
|
||||||
|
|
||||||
src := html.EscapeString(item.UserAvatarLink)
|
avatarHTML := templates.AvatarHTML(item.UserAvatarLink, 24, avatars.DefaultAvatarClass+" tw-mr-2", userName)
|
||||||
class := avatars.DefaultAvatarClass + " tw-mr-2"
|
timeSinceHTML := templates.TimeSince(item.EditedUnix)
|
||||||
name := html.EscapeString(username)
|
|
||||||
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
|
|
||||||
timeSinceHTML := string(templates.TimeSince(item.EditedUnix))
|
|
||||||
|
|
||||||
results = append(results, map[string]any{
|
results = append(results, map[string]any{
|
||||||
"name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceHTML,
|
"name": htmlutil.HTMLFormat("%s <strong>%s</strong>%s %s %s", avatarHTML, userName, fullNameHTML, actionHTML, timeSinceHTML),
|
||||||
"value": item.HistoryID,
|
"value": item.HistoryID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
@@ -34,7 +33,7 @@ func IssuePullPosters(ctx *context.Context) {
|
|||||||
func issuePosters(ctx *context.Context, isPullList bool) {
|
func issuePosters(ctx *context.Context, isPullList bool) {
|
||||||
repo := ctx.Repo.Repository
|
repo := ctx.Repo.Repository
|
||||||
search := strings.TrimSpace(ctx.FormString("q"))
|
search := strings.TrimSpace(ctx.FormString("q"))
|
||||||
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
|
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, err)
|
ctx.JSON(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -54,9 +53,7 @@ func issuePosters(ctx *context.Context, isPullList bool) {
|
|||||||
resp.Results = make([]*userSearchInfo, len(posters))
|
resp.Results = make([]*userSearchInfo, len(posters))
|
||||||
for i, user := range posters {
|
for i, user := range posters {
|
||||||
resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
|
resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
|
||||||
if setting.UI.DefaultShowFullName {
|
resp.Results[i].FullName = user.FullName
|
||||||
resp.Results[i].FullName = user.FullName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,18 +158,23 @@ func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ct
|
|||||||
|
|
||||||
func fromDisplayName(u *user_model.User) string {
|
func fromDisplayName(u *user_model.User) string {
|
||||||
if setting.MailService.FromDisplayNameFormatTemplate != nil {
|
if setting.MailService.FromDisplayNameFormatTemplate != nil {
|
||||||
var ctx bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
|
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{
|
||||||
"DisplayName": u.DisplayName(),
|
"DisplayName": u.DisplayName(),
|
||||||
"AppName": setting.AppName,
|
"AppName": setting.AppName,
|
||||||
"Domain": setting.Domain,
|
"Domain": setting.Domain,
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return mime.QEncoding.Encode("utf-8", ctx.String())
|
return mime.QEncoding.Encode("utf-8", buf.String())
|
||||||
}
|
}
|
||||||
log.Error("fromDisplayName: %w", err)
|
log.Error("fromDisplayName: %w", err)
|
||||||
}
|
}
|
||||||
return u.GetCompleteName()
|
def := u.Name
|
||||||
|
if fullName := strings.TrimSpace(u.FullName); fullName != "" {
|
||||||
|
// use "Full Name (username)" for email's sender name if Full Name is not empty
|
||||||
|
def = fullName + " (" + u.Name + ")"
|
||||||
|
}
|
||||||
|
return def
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
|
func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
|
||||||
|
|||||||
@@ -48,11 +48,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Users}}
|
{{range $org := .Users}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.ID}}</td>
|
<td>{{.ID}}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{.HomeLink}}">{{if and DefaultShowFullName .FullName}}{{.FullName}} ({{.Name}}){{else}}{{.Name}}{{end}}</a>
|
<span class="username-display">
|
||||||
|
<a href="{{$org.HomeLink}}">{{$org.Name}}</a>
|
||||||
|
{{if $org.FullName}}<span class="username-fullname">({{$org.FullName}})</span>{{end}}
|
||||||
|
</span>
|
||||||
{{if .Visibility.IsPrivate}}
|
{{if .Visibility.IsPrivate}}
|
||||||
<span class="tw-text-gold">{{svg "octicon-lock"}}</span>
|
<span class="tw-text-gold">{{svg "octicon-lock"}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -14,18 +14,15 @@
|
|||||||
{{range .Commits}}
|
{{range .Commits}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="author">
|
<td class="author">
|
||||||
<div class="tw-flex">
|
<span class="author-wrapper">
|
||||||
{{$userName := .Author.Name}}
|
{{- if .User -}}
|
||||||
{{if .User}}
|
{{- ctx.AvatarUtils.Avatar .User 20 "tw-mr-2" -}}
|
||||||
{{if and .User.FullName DefaultShowFullName}}
|
{{- .User.GetShortDisplayNameLinkHTML -}}
|
||||||
{{$userName = .User.FullName}}
|
{{- else -}}
|
||||||
{{end}}
|
{{- ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20 "tw-mr-2" -}}
|
||||||
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a>
|
{{- .Author.Name -}}
|
||||||
{{else}}
|
{{- end -}}
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}}
|
</span>
|
||||||
<span class="author-wrapper">{{$userName}}</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="sha">
|
<td class="sha">
|
||||||
{{$commitBaseLink := ""}}
|
{{$commitBaseLink := ""}}
|
||||||
|
|||||||
@@ -41,16 +41,13 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="flex-text-inline tw-text-12">
|
<span class="flex-text-inline tw-text-12">
|
||||||
{{$userName := $commit.Commit.Author.Name}}
|
|
||||||
{{if $commit.User}}
|
{{if $commit.User}}
|
||||||
{{if and $commit.User.FullName DefaultShowFullName}}
|
|
||||||
{{$userName = $commit.User.FullName}}
|
|
||||||
{{end}}
|
|
||||||
{{ctx.AvatarUtils.Avatar $commit.User 18}}
|
{{ctx.AvatarUtils.Avatar $commit.User 18}}
|
||||||
<a class="muted" href="{{$commit.User.HomeLink}}">{{$userName}}</a>
|
{{$commit.User.GetShortDisplayNameLinkHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName 18}}
|
{{$gitUserName := $commit.Commit.Author.Name}}
|
||||||
{{$userName}}
|
{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $gitUserName 18}}
|
||||||
|
{{$gitUserName}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{$queryLink := .QueryLink}}
|
{{$queryLink := .QueryLink}}
|
||||||
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
|
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
|
||||||
{{$.TextFilterTitle}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{$.TextFilterTitle}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu flex-items-menu">
|
||||||
<div class="ui icon search input">
|
<div class="ui icon search input">
|
||||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}">
|
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}">
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
{{if not .LatestCommit}}
|
{{if not .LatestCommit}}
|
||||||
…
|
…
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .LatestCommitUser}}
|
<span class="author-wrapper">
|
||||||
{{ctx.AvatarUtils.Avatar .LatestCommitUser 24}}
|
{{- if .LatestCommitUser -}}
|
||||||
{{if and .LatestCommitUser.FullName DefaultShowFullName}}
|
{{- ctx.AvatarUtils.Avatar .LatestCommitUser 20 "tw-mr-2" -}}
|
||||||
<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
|
<strong>{{.LatestCommitUser.GetShortDisplayNameLinkHTML}}</strong>
|
||||||
{{else}}
|
{{- else if .LatestCommit.Author -}}
|
||||||
<a class="muted author-wrapper" title="{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
|
{{- ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 20 "tw-mr-2" -}}
|
||||||
{{end}}
|
<strong>{{.LatestCommit.Author.Name}}</strong>
|
||||||
{{else}}
|
{{- end -}}
|
||||||
{{if .LatestCommit.Author}}
|
</span>
|
||||||
{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}}
|
|
||||||
<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<span class="gt-ellipsis">{{.Name}}{{if DefaultShowFullName}}<span class="search-fullname"> {{.FullName}}</span>{{end}}</span>
|
<span class="username-display">{{.Name}} {{if .FullName}}<span class="username-fullname gt-ellipsis">({{.FullName}})</span>{{end}}</span>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ func TestRepoCommits(t *testing.T) {
|
|||||||
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
|
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
|
||||||
commits = append(commits, path.Base(s.AttrOr("href", "")))
|
commits = append(commits, path.Base(s.AttrOr("href", "")))
|
||||||
})
|
})
|
||||||
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
|
doc.doc.Find("#commits-table .author-wrapper a").Each(func(i int, s *goquery.Selection) {
|
||||||
userHrefs = append(userHrefs, s.AttrOr("href", ""))
|
userHrefs = append(userHrefs, s.AttrOr("href", ""))
|
||||||
})
|
})
|
||||||
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
|
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
|
||||||
@@ -49,7 +50,7 @@ func TestRepoCommits(t *testing.T) {
|
|||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
doc := NewHTMLParser(t, resp.Body)
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
||||||
authorHref := doc.doc.Find(".latest-commit .author-wrapper").AttrOr("href", "")
|
authorHref := doc.doc.Find(".latest-commit .author-wrapper a").AttrOr("href", "")
|
||||||
assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref)
|
assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref)
|
||||||
assert.Equal(t, "/user2", authorHref)
|
assert.Equal(t, "/user2", authorHref)
|
||||||
})
|
})
|
||||||
@@ -65,8 +66,7 @@ func TestRepoCommits(t *testing.T) {
|
|||||||
commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "")
|
commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "")
|
||||||
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
||||||
authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper")
|
authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper")
|
||||||
assert.Equal(t, "6543", authorElem.Text())
|
assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text()))
|
||||||
assert.Equal(t, "span", authorElem.Nodes[0].Data)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("LastCommitNonExistingCommiter", func(t *testing.T) {
|
t.Run("LastCommitNonExistingCommiter", func(t *testing.T) {
|
||||||
@@ -76,8 +76,7 @@ func TestRepoCommits(t *testing.T) {
|
|||||||
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
||||||
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
||||||
authorElem := doc.doc.Find(".latest-commit .author-wrapper")
|
authorElem := doc.doc.Find(".latest-commit .author-wrapper")
|
||||||
assert.Equal(t, "6543", authorElem.Text())
|
assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text()))
|
||||||
assert.Equal(t, "span", authorElem.Nodes[0].Data)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1969,7 +1969,15 @@ tbody.commit-list {
|
|||||||
padding-right: 0.5em; /* To match the alignment with the "required" label */
|
padding-right: 0.5em; /* To match the alignment with the "required" label */
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-fullname {
|
.username-display {
|
||||||
|
max-width: 100%; /* the inner part might have "gt-ellipsis" */
|
||||||
|
min-width: 0; /* if it is the top flex container, "max-width" works; but if it is inside another flex container, the parent needs to handle the x-axis and here also needs "min-width" */
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--gap-inline);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-display > .username-fullname {
|
||||||
color: var(--color-text-light-2);
|
color: var(--color-text-light-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {updateIssuesMeta} from './repo-common.ts';
|
import {updateIssuesMeta} from './repo-common.ts';
|
||||||
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
|
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
|
||||||
import {html} from '../utils/html.ts';
|
import {html, htmlRaw} from '../utils/html.ts';
|
||||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||||
import {showErrorToast} from '../modules/toast.ts';
|
import {showErrorToast} from '../modules/toast.ts';
|
||||||
import {createSortable} from '../modules/sortable.ts';
|
import {createSortable} from '../modules/sortable.ts';
|
||||||
@@ -138,10 +138,11 @@ function initDropdownUserRemoteSearch(el: Element) {
|
|||||||
// the content is provided by backend IssuePosters handler
|
// the content is provided by backend IssuePosters handler
|
||||||
processedResults.length = 0;
|
processedResults.length = 0;
|
||||||
for (const item of resp.results) {
|
for (const item of resp.results) {
|
||||||
let nameHtml = html`<img class="ui avatar tw-align-middle" src="${item.avatar_link}" aria-hidden="true" alt width="20" height="20"><span class="gt-ellipsis">${item.username}</span>`;
|
const htmlAvatar = html`<img class="ui avatar tw-align-middle" src="${item.avatar_link}" aria-hidden="true" alt width="20" height="20">`;
|
||||||
if (item.full_name) nameHtml += html`<span class="search-fullname tw-ml-2">${item.full_name}</span>`;
|
const htmlFullName = item.full_name ? html`<span class="username-fullname gt-ellipsis">(${item.full_name})</span>` : '';
|
||||||
|
const htmlItem = html`<span class="username-display">${htmlRaw(htmlAvatar)}<span>${item.username}</span>${htmlRaw(htmlFullName)}</span>`;
|
||||||
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
||||||
processedResults.push({value: item.username, name: nameHtml});
|
processedResults.push({value: item.username, name: htmlItem});
|
||||||
}
|
}
|
||||||
resp.results = processedResults;
|
resp.results = processedResults;
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
Reference in New Issue
Block a user