1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-08-05 18:05:19 +02:00

feat(ui): add issue comment assignment doer links

This commit is contained in:
Robert Wolff 2025-06-23 23:00:32 +02:00
parent 39e6785da0
commit 1b9ac27578
5 changed files with 58 additions and 25 deletions

View file

@ -188,6 +188,7 @@ func NewFuncMap() template.FuncMap {
"RenderMarkdownToHtml": RenderMarkdownToHtml, "RenderMarkdownToHtml": RenderMarkdownToHtml,
"RenderLabel": RenderLabel, "RenderLabel": RenderLabel,
"RenderLabels": RenderLabels, "RenderLabels": RenderLabels,
"RenderUser": RenderUser,
"RenderReviewRequest": RenderReviewRequest, "RenderReviewRequest": RenderReviewRequest,
// ----------------------------------------------------------------- // -----------------------------------------------------------------

View file

@ -7,6 +7,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"html"
"html/template" "html/template"
"math" "math"
"net/url" "net/url"
@ -15,6 +16,7 @@ import (
"unicode" "unicode"
issues_model "forgejo.org/models/issues" issues_model "forgejo.org/models/issues"
user_model "forgejo.org/models/user"
"forgejo.org/modules/emoji" "forgejo.org/modules/emoji"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/markup" "forgejo.org/modules/markup"
@ -26,7 +28,7 @@ import (
// RenderCommitMessage renders commit message with XSS-safe and special links. // RenderCommitMessage renders commit message with XSS-safe and special links.
func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML { func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
cleanMsg := template.HTMLEscapeString(msg) cleanMsg := html.EscapeString(msg)
// we can safely assume that it will not return any error, since there // we can safely assume that it will not return any error, since there
// shouldn't be any special HTML. // shouldn't be any special HTML.
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
@ -63,7 +65,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
Ctx: ctx, Ctx: ctx,
DefaultLink: urlDefault, DefaultLink: urlDefault,
Metas: metas, Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, html.EscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessageSubject: %v", err) log.Error("RenderCommitMessageSubject: %v", err)
return template.HTML("") return template.HTML("")
@ -88,7 +90,7 @@ func RenderCommitBody(ctx context.Context, msg string, metas map[string]string)
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Metas: metas, Metas: metas,
}, template.HTMLEscapeString(msgLine)) }, html.EscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("RenderCommitMessage: %v", err)
return "" return ""
@ -122,7 +124,7 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Metas: metas, Metas: metas,
}, template.HTMLEscapeString(text)) }, html.EscapeString(text))
if err != nil { if err != nil {
log.Error("RenderIssueTitle: %v", err) log.Error("RenderIssueTitle: %v", err)
return template.HTML("") return template.HTML("")
@ -132,7 +134,7 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
// RenderRefIssueTitle renders referenced issue/pull title with defined post processors // RenderRefIssueTitle renders referenced issue/pull title with defined post processors
func RenderRefIssueTitle(ctx context.Context, text string) template.HTML { func RenderRefIssueTitle(ctx context.Context, text string) template.HTML {
renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, template.HTMLEscapeString(text)) renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, html.EscapeString(text))
if err != nil { if err != nil {
log.Error("RenderRefIssueTitle: %v", err) log.Error("RenderRefIssueTitle: %v", err)
return "" return ""
@ -150,7 +152,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
labelScope = label.ExclusiveScope() labelScope = label.ExclusiveScope()
) )
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) description := emoji.ReplaceAliases(html.EscapeString(label.Description))
if label.IsArchived() { if label.IsArchived() {
archivedCSSClass = "archived-label" archivedCSSClass = "archived-label"
@ -212,7 +214,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
// RenderEmoji renders html text with emoji post processors // RenderEmoji renders html text with emoji post processors
func RenderEmoji(ctx context.Context, text string) template.HTML { func RenderEmoji(ctx context.Context, text string) template.HTML {
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx}, renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
template.HTMLEscapeString(text)) html.EscapeString(text))
if err != nil { if err != nil {
log.Error("RenderEmoji: %v", err) log.Error("RenderEmoji: %v", err)
return template.HTML("") return template.HTML("")
@ -263,10 +265,20 @@ func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issu
return template.HTML(htmlCode) return template.HTML(htmlCode)
} }
func RenderUser(ctx context.Context, user user_model.User) template.HTML {
if user.ID > 0 {
return template.HTML(fmt.Sprintf(
"<a href='%s' rel='nofollow'><strong>%s</strong></a>",
user.HomeLink(), html.EscapeString(user.GetDisplayName())))
}
return template.HTML(fmt.Sprintf("<strong>%s</strong>",
html.EscapeString(user.GetDisplayName())))
}
func RenderReviewRequest(users []issues_model.RequestReviewTarget) template.HTML { func RenderReviewRequest(users []issues_model.RequestReviewTarget) template.HTML {
usernames := make([]string, 0, len(users)) usernames := make([]string, 0, len(users))
for _, user := range users { for _, user := range users {
usernames = append(usernames, template.HTMLEscapeString(user.Name())) usernames = append(usernames, html.EscapeString(user.Name()))
} }
htmlCode := `<span class="review-request-list">` htmlCode := `<span class="review-request-list">`

View file

@ -11,6 +11,9 @@ import (
"forgejo.org/models/db" "forgejo.org/models/db"
issues_model "forgejo.org/models/issues" issues_model "forgejo.org/models/issues"
"forgejo.org/models/unittest" "forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/translation" "forgejo.org/modules/translation"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -221,3 +224,26 @@ func TestRenderLabels(t *testing.T) {
assert.Contains(t, RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", true), assert.Contains(t, RenderLabels(db.DefaultContext, tr, []*issues_model.Label{label}, "user2/repo1", true),
"user2/repo1/pulls?labels=1") "user2/repo1/pulls?labels=1")
} }
func TestRenderUser(t *testing.T) {
unittest.PrepareTestEnv(t)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
ghost := user_model.NewGhostUser()
assert.Contains(t, RenderUser(db.DefaultContext, *user),
"<a href='/user2' rel='nofollow'><strong>user2</strong></a>")
assert.Contains(t, RenderUser(db.DefaultContext, *org),
"<a href='/org3' rel='nofollow'><strong>org3</strong></a>")
assert.Contains(t, RenderUser(db.DefaultContext, *ghost),
"<strong>Ghost</strong>")
defer test.MockVariableValue(&setting.UI.DefaultShowFullName, true)()
assert.Contains(t, RenderUser(db.DefaultContext, *user),
"<a href='/user2' rel='nofollow'><strong>&lt; U&lt;se&gt;r Tw&lt;o &gt; &gt;&lt;</strong></a>")
assert.Contains(t, RenderUser(db.DefaultContext, *org),
"<a href='/org3' rel='nofollow'><strong>&lt;&lt;&lt;&lt; &gt;&gt; &gt;&gt; &gt; &gt;&gt; &gt; &gt;&gt;&gt; &gt;&gt;</strong></a>")
assert.Contains(t, RenderUser(db.DefaultContext, *ghost),
"<strong>Ghost</strong>")
}

View file

@ -197,27 +197,23 @@
{{else if and (eq .Type 9) (gt .AssigneeID 0)}} {{else if and (eq .Type 9) (gt .AssigneeID 0)}}
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-person"}}</span> <span class="badge">{{svg "octicon-person"}}</span>
{{if .RemovedAssignee}}
{{template "shared/user/avatarlink" dict "user" .Assignee}} {{template "shared/user/avatarlink" dict "user" .Assignee}}
<span class="text grey muted-links"> <span class="text grey muted-links">
{{template "shared/user/authorlink" .Assignee}} {{template "shared/user/authorlink" .Assignee}}
{{if .RemovedAssignee}}
{{if eq .Poster.ID .Assignee.ID}} {{if eq .Poster.ID .Assignee.ID}}
{{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}} {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}}
{{else}} {{else}}
{{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}} {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (RenderUser $.Context .Poster) $createdStr}}
{{end}} {{end}}
</span>
{{else}} {{else}}
{{template "shared/user/avatarlink" dict "user" .Assignee}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Assignee}}
{{if eq .Poster.ID .AssigneeID}} {{if eq .Poster.ID .AssigneeID}}
{{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}} {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}}
{{else}} {{else}}
{{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}} {{ctx.Locale.Tr "repo.issues.add_assignee_at" (RenderUser $.Context .Poster) $createdStr}}
{{end}}
{{end}} {{end}}
</span> </span>
{{end}}
</div> </div>
{{else if eq .Type 10}} {{else if eq .Type 10}}
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">

View file

@ -205,15 +205,13 @@ func TestIssueCommentChangeAssignee(t *testing.T) {
testIssueCommentChangeEvent(t, htmlDoc, "2041", testIssueCommentChangeEvent(t, htmlDoc, "2041",
"octicon-person", "User One", "/user1", "octicon-person", "User One", "/user1",
[]string{"user1 was unassigned by user2"}, []string{"user1 was unassigned by user2"},
[]string{"/user1"}) []string{"/user1", "/user2"})
// []string{"/user1", "/user2"})
// Add other // Add other
testIssueCommentChangeEvent(t, htmlDoc, "2042", testIssueCommentChangeEvent(t, htmlDoc, "2042",
"octicon-person", "< U<se>r Tw<o > ><", "/user2", "octicon-person", "< U<se>r Tw<o > ><", "/user2",
[]string{"user2 was assigned by user1"}, []string{"user2 was assigned by user1"},
[]string{"/user2"}) []string{"/user2", "/user1"})
// []string{"/user2", "/user1"})
// Self-remove // Self-remove
testIssueCommentChangeEvent(t, htmlDoc, "2043", testIssueCommentChangeEvent(t, htmlDoc, "2043",