mirror of https://github.com/go-gitea/gitea.git
Group template helper functions, remove `Printf`, improve template error messages (#23982)
Follow #23328 Major changes: * Group the function in `templates/help.go` by their purposes. It could make future work easier. * Remove the `Printf` helper function, there is already a builtin `printf`. * Remove `DiffStatsWidth`, replace with `Eval` in template * Rename the `NewTextFuncMap` to `mailSubjectTextFuncMap`, it's for subject text template only, no need to make it support HTML functions. ---- And fine tune template error messages, to make it more friendly to developers and users. ![image](https://user-images.githubusercontent.com/2114189/230714245-4fd202d1-2b25-41b2-8be5-03c5fee45091.png) ![image](https://user-images.githubusercontent.com/2114189/230714277-66783577-2a03-49d5-8e8c-ceba5e07a2d4.png) --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
parent
cf5a281fdc
commit
fdbd646113
|
@ -47,7 +47,7 @@ import (
|
||||||
|
|
||||||
// Render represents a template render
|
// Render represents a template render
|
||||||
type Render interface {
|
type Render interface {
|
||||||
TemplateLookup(tmpl string) *template.Template
|
TemplateLookup(tmpl string) (*template.Template, error)
|
||||||
HTML(w io.Writer, status int, name string, data interface{}) error
|
HTML(w io.Writer, status int, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +228,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||||
}
|
}
|
||||||
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
|
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
|
||||||
if status == http.StatusInternalServerError && name == base.TplName("status/500") {
|
if status == http.StatusInternalServerError && name == base.TplName("status/500") {
|
||||||
ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template")
|
ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if execErr, ok := err.(texttemplate.ExecError); ok {
|
if execErr, ok := err.(texttemplate.ExecError); ok {
|
||||||
|
@ -247,7 +247,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||||
if errorTemplateName != string(name) {
|
if errorTemplateName != string(name) {
|
||||||
filename += " (subtemplate of " + string(name) + ")"
|
filename += " (subtemplate of " + string(name) + ")"
|
||||||
}
|
}
|
||||||
err = fmt.Errorf("%w\nin template file %s:\n%s", err, filename, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
|
err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
|
||||||
} else {
|
} else {
|
||||||
filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
|
filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
|
||||||
if filenameErr != nil {
|
if filenameErr != nil {
|
||||||
|
@ -256,7 +256,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||||
if execErr.Name != string(name) {
|
if execErr.Name != string(name) {
|
||||||
filename += " (subtemplate of " + string(name) + ")"
|
filename += " (subtemplate of " + string(name) + ")"
|
||||||
}
|
}
|
||||||
err = fmt.Errorf("%w\nin template file %s", err, filename)
|
err = fmt.Errorf("failed to render %s, error: %w", filename, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.ServerError("Render failed", err)
|
ctx.ServerError("Render failed", err)
|
||||||
|
|
|
@ -6,20 +6,13 @@
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
texttmpl "text/template"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
subjectTemplates = texttmpl.New("")
|
|
||||||
bodyTemplates = template.New("")
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetAsset returns asset content via name
|
// GetAsset returns asset content via name
|
||||||
func GetAsset(name string) ([]byte, error) {
|
func GetAsset(name string) ([]byte, error) {
|
||||||
bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
|
bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
@ -55,6 +54,134 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
||||||
// NewFuncMap returns functions for injecting to templates
|
// NewFuncMap returns functions for injecting to templates
|
||||||
func NewFuncMap() []template.FuncMap {
|
func NewFuncMap() []template.FuncMap {
|
||||||
return []template.FuncMap{map[string]interface{}{
|
return []template.FuncMap{map[string]interface{}{
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// html/template related functions
|
||||||
|
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
|
||||||
|
"Eval": Eval,
|
||||||
|
"Safe": Safe,
|
||||||
|
"Escape": html.EscapeString,
|
||||||
|
"QueryEscape": url.QueryEscape,
|
||||||
|
"JSEscape": template.JSEscapeString,
|
||||||
|
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
|
||||||
|
"URLJoin": util.URLJoin,
|
||||||
|
|
||||||
|
"PathEscape": url.PathEscape,
|
||||||
|
"PathEscapeSegments": util.PathEscapeSegments,
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// string / json
|
||||||
|
"Join": strings.Join,
|
||||||
|
"DotEscape": DotEscape,
|
||||||
|
"HasPrefix": strings.HasPrefix,
|
||||||
|
"EllipsisString": base.EllipsisString,
|
||||||
|
|
||||||
|
"Json": func(in interface{}) string {
|
||||||
|
out, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
},
|
||||||
|
"JsonPrettyPrint": func(in string) string {
|
||||||
|
var out bytes.Buffer
|
||||||
|
err := json.Indent(&out, []byte(in), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// svg / avatar / icon
|
||||||
|
"svg": svg.RenderHTML,
|
||||||
|
"avatar": Avatar,
|
||||||
|
"avatarHTML": AvatarHTML,
|
||||||
|
"avatarByAction": AvatarByAction,
|
||||||
|
"avatarByEmail": AvatarByEmail,
|
||||||
|
"repoAvatar": RepoAvatar,
|
||||||
|
"EntryIcon": base.EntryIcon,
|
||||||
|
"MigrationIcon": MigrationIcon,
|
||||||
|
"ActionIcon": ActionIcon,
|
||||||
|
|
||||||
|
"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
|
||||||
|
// if needed
|
||||||
|
if len(normSort) == 0 || len(urlSort) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urlSort) == 0 && isDefault {
|
||||||
|
// if sort is sorted as default add arrow tho this table header
|
||||||
|
if isDefault {
|
||||||
|
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if sort arg is in url test if it correlates with column header sort arguments
|
||||||
|
// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
|
||||||
|
if urlSort == normSort {
|
||||||
|
// the table is sorted with this header normal
|
||||||
|
return svg.RenderHTML("octicon-triangle-up", 16)
|
||||||
|
} else if urlSort == revSort {
|
||||||
|
// the table is sorted with this header reverse
|
||||||
|
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the table is NOT sorted with this header
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// time / number / format
|
||||||
|
"FileSize": base.FileSize,
|
||||||
|
"LocaleNumber": LocaleNumber,
|
||||||
|
"CountFmt": base.FormatNumberSI,
|
||||||
|
"TimeSince": timeutil.TimeSince,
|
||||||
|
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||||
|
"Sec2Time": util.SecToTime,
|
||||||
|
"DateFmtLong": func(t time.Time) string {
|
||||||
|
return t.Format(time.RFC1123Z)
|
||||||
|
},
|
||||||
|
"LoadTimes": func(startTime time.Time) string {
|
||||||
|
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// slice
|
||||||
|
"containGeneric": func(arr, v interface{}) bool {
|
||||||
|
arrV := reflect.ValueOf(arr)
|
||||||
|
if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
|
||||||
|
return strings.Contains(arr.(string), v.(string))
|
||||||
|
}
|
||||||
|
if arrV.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < arrV.Len(); i++ {
|
||||||
|
iV := arrV.Index(i)
|
||||||
|
if !iV.CanInterface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iV.Interface() == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
"contain": func(s []int64, id int64) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
"Iterate": func(arg interface{}) (items []int64) {
|
||||||
|
count, _ := util.ToInt64(arg)
|
||||||
|
for i := int64(0); i < count; i++ {
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// setting
|
||||||
"AppName": func() string {
|
"AppName": func() string {
|
||||||
return setting.AppName
|
return setting.AppName
|
||||||
},
|
},
|
||||||
|
@ -89,56 +216,12 @@ func NewFuncMap() []template.FuncMap {
|
||||||
"ShowFooterTemplateLoadTime": func() bool {
|
"ShowFooterTemplateLoadTime": func() bool {
|
||||||
return setting.ShowFooterTemplateLoadTime
|
return setting.ShowFooterTemplateLoadTime
|
||||||
},
|
},
|
||||||
"LoadTimes": func(startTime time.Time) string {
|
|
||||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
|
||||||
},
|
|
||||||
"AllowedReactions": func() []string {
|
"AllowedReactions": func() []string {
|
||||||
return setting.UI.Reactions
|
return setting.UI.Reactions
|
||||||
},
|
},
|
||||||
"CustomEmojis": func() map[string]string {
|
"CustomEmojis": func() map[string]string {
|
||||||
return setting.UI.CustomEmojisMap
|
return setting.UI.CustomEmojisMap
|
||||||
},
|
},
|
||||||
"Safe": Safe,
|
|
||||||
"JSEscape": JSEscape,
|
|
||||||
"Str2html": Str2html,
|
|
||||||
"TimeSince": timeutil.TimeSince,
|
|
||||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
|
||||||
"FileSize": base.FileSize,
|
|
||||||
"LocaleNumber": LocaleNumber,
|
|
||||||
"EntryIcon": base.EntryIcon,
|
|
||||||
"MigrationIcon": MigrationIcon,
|
|
||||||
"ActionIcon": ActionIcon,
|
|
||||||
"DateFmtLong": func(t time.Time) string {
|
|
||||||
return t.Format(time.RFC1123Z)
|
|
||||||
},
|
|
||||||
"CountFmt": base.FormatNumberSI,
|
|
||||||
"EllipsisString": base.EllipsisString,
|
|
||||||
"DiffLineTypeToStr": DiffLineTypeToStr,
|
|
||||||
"ShortSha": base.ShortSha,
|
|
||||||
"ActionContent2Commits": ActionContent2Commits,
|
|
||||||
"PathEscape": url.PathEscape,
|
|
||||||
"PathEscapeSegments": util.PathEscapeSegments,
|
|
||||||
"URLJoin": util.URLJoin,
|
|
||||||
"RenderCommitMessage": RenderCommitMessage,
|
|
||||||
"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
|
|
||||||
"RenderCommitBody": RenderCommitBody,
|
|
||||||
"RenderCodeBlock": RenderCodeBlock,
|
|
||||||
"RenderIssueTitle": RenderIssueTitle,
|
|
||||||
"RenderEmoji": RenderEmoji,
|
|
||||||
"RenderEmojiPlain": emoji.ReplaceAliases,
|
|
||||||
"ReactionToEmoji": ReactionToEmoji,
|
|
||||||
"RenderNote": RenderNote,
|
|
||||||
"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
|
|
||||||
output, err := markdown.RenderString(&markup.RenderContext{
|
|
||||||
Ctx: ctx,
|
|
||||||
URLPrefix: setting.AppSubURL,
|
|
||||||
}, input)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("RenderString: %v", err)
|
|
||||||
}
|
|
||||||
return template.HTML(output)
|
|
||||||
},
|
|
||||||
"IsMultilineCommitMessage": IsMultilineCommitMessage,
|
|
||||||
"ThemeColorMetaTag": func() string {
|
"ThemeColorMetaTag": func() string {
|
||||||
return setting.UI.ThemeColorMetaTag
|
return setting.UI.ThemeColorMetaTag
|
||||||
},
|
},
|
||||||
|
@ -157,6 +240,82 @@ func NewFuncMap() []template.FuncMap {
|
||||||
"EnableTimetracking": func() bool {
|
"EnableTimetracking": func() bool {
|
||||||
return setting.Service.EnableTimetracking
|
return setting.Service.EnableTimetracking
|
||||||
},
|
},
|
||||||
|
"DisableGitHooks": func() bool {
|
||||||
|
return setting.DisableGitHooks
|
||||||
|
},
|
||||||
|
"DisableWebhooks": func() bool {
|
||||||
|
return setting.DisableWebhooks
|
||||||
|
},
|
||||||
|
"DisableImportLocal": func() bool {
|
||||||
|
return !setting.ImportLocalPaths
|
||||||
|
},
|
||||||
|
"DefaultTheme": func() string {
|
||||||
|
return setting.UI.DefaultTheme
|
||||||
|
},
|
||||||
|
"NotificationSettings": func() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
||||||
|
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
||||||
|
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
||||||
|
"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MermaidMaxSourceCharacters": func() int {
|
||||||
|
return setting.MermaidMaxSourceCharacters
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// render
|
||||||
|
"RenderCommitMessage": RenderCommitMessage,
|
||||||
|
"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
|
||||||
|
|
||||||
|
"RenderCommitBody": RenderCommitBody,
|
||||||
|
"RenderCodeBlock": RenderCodeBlock,
|
||||||
|
"RenderIssueTitle": RenderIssueTitle,
|
||||||
|
"RenderEmoji": RenderEmoji,
|
||||||
|
"RenderEmojiPlain": emoji.ReplaceAliases,
|
||||||
|
"ReactionToEmoji": ReactionToEmoji,
|
||||||
|
"RenderNote": RenderNote,
|
||||||
|
|
||||||
|
"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
|
||||||
|
output, err := markdown.RenderString(&markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
|
URLPrefix: setting.AppSubURL,
|
||||||
|
}, input)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("RenderString: %v", err)
|
||||||
|
}
|
||||||
|
return template.HTML(output)
|
||||||
|
},
|
||||||
|
"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
|
||||||
|
return template.HTML(RenderLabel(ctx, label))
|
||||||
|
},
|
||||||
|
"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
||||||
|
htmlCode := `<span class="labels-list">`
|
||||||
|
for _, label := range labels {
|
||||||
|
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
||||||
|
if label == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
||||||
|
repoLink, label.ID, RenderLabel(ctx, label))
|
||||||
|
}
|
||||||
|
htmlCode += "</span>"
|
||||||
|
return template.HTML(htmlCode)
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// misc
|
||||||
|
"DiffLineTypeToStr": DiffLineTypeToStr,
|
||||||
|
"ShortSha": base.ShortSha,
|
||||||
|
"ActionContent2Commits": ActionContent2Commits,
|
||||||
|
"IsMultilineCommitMessage": IsMultilineCommitMessage,
|
||||||
|
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
||||||
|
"MirrorRemoteAddress": mirrorRemoteAddress,
|
||||||
|
|
||||||
|
"ParseDeadline": func(deadline string) []string {
|
||||||
|
return strings.Split(deadline, "|")
|
||||||
|
},
|
||||||
"FilenameIsImage": func(filename string) bool {
|
"FilenameIsImage": func(filename string) bool {
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
||||||
return strings.HasPrefix(mimeType, "image/")
|
return strings.HasPrefix(mimeType, "image/")
|
||||||
|
@ -191,142 +350,6 @@ func NewFuncMap() []template.FuncMap {
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
},
|
},
|
||||||
"DiffStatsWidth": func(adds, dels int) string {
|
|
||||||
return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100)
|
|
||||||
},
|
|
||||||
"Json": func(in interface{}) string {
|
|
||||||
out, err := json.Marshal(in)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(out)
|
|
||||||
},
|
|
||||||
"JsonPrettyPrint": func(in string) string {
|
|
||||||
var out bytes.Buffer
|
|
||||||
err := json.Indent(&out, []byte(in), "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return out.String()
|
|
||||||
},
|
|
||||||
"DisableGitHooks": func() bool {
|
|
||||||
return setting.DisableGitHooks
|
|
||||||
},
|
|
||||||
"DisableWebhooks": func() bool {
|
|
||||||
return setting.DisableWebhooks
|
|
||||||
},
|
|
||||||
"DisableImportLocal": func() bool {
|
|
||||||
return !setting.ImportLocalPaths
|
|
||||||
},
|
|
||||||
"Printf": fmt.Sprintf,
|
|
||||||
"Escape": Escape,
|
|
||||||
"Sec2Time": util.SecToTime,
|
|
||||||
"ParseDeadline": func(deadline string) []string {
|
|
||||||
return strings.Split(deadline, "|")
|
|
||||||
},
|
|
||||||
"DefaultTheme": func() string {
|
|
||||||
return setting.UI.DefaultTheme
|
|
||||||
},
|
|
||||||
"dict": dict,
|
|
||||||
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
|
||||||
"MirrorRemoteAddress": mirrorRemoteAddress,
|
|
||||||
"NotificationSettings": func() map[string]interface{} {
|
|
||||||
return map[string]interface{}{
|
|
||||||
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
|
||||||
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
|
||||||
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
|
||||||
"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"containGeneric": func(arr, v interface{}) bool {
|
|
||||||
arrV := reflect.ValueOf(arr)
|
|
||||||
if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
|
|
||||||
return strings.Contains(arr.(string), v.(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
if arrV.Kind() == reflect.Slice {
|
|
||||||
for i := 0; i < arrV.Len(); i++ {
|
|
||||||
iV := arrV.Index(i)
|
|
||||||
if !iV.CanInterface() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if iV.Interface() == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
"contain": func(s []int64, id int64) bool {
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] == id {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
"svg": svg.RenderHTML,
|
|
||||||
"avatar": Avatar,
|
|
||||||
"avatarHTML": AvatarHTML,
|
|
||||||
"avatarByAction": AvatarByAction,
|
|
||||||
"avatarByEmail": AvatarByEmail,
|
|
||||||
"repoAvatar": RepoAvatar,
|
|
||||||
"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
|
|
||||||
// if needed
|
|
||||||
if len(normSort) == 0 || len(urlSort) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(urlSort) == 0 && isDefault {
|
|
||||||
// if sort is sorted as default add arrow tho this table header
|
|
||||||
if isDefault {
|
|
||||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if sort arg is in url test if it correlates with column header sort arguments
|
|
||||||
// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
|
|
||||||
if urlSort == normSort {
|
|
||||||
// the table is sorted with this header normal
|
|
||||||
return svg.RenderHTML("octicon-triangle-up", 16)
|
|
||||||
} else if urlSort == revSort {
|
|
||||||
// the table is sorted with this header reverse
|
|
||||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the table is NOT sorted with this header
|
|
||||||
return ""
|
|
||||||
},
|
|
||||||
"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
|
|
||||||
return template.HTML(RenderLabel(ctx, label))
|
|
||||||
},
|
|
||||||
"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
|
||||||
htmlCode := `<span class="labels-list">`
|
|
||||||
for _, label := range labels {
|
|
||||||
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
|
||||||
if label == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
|
||||||
repoLink, label.ID, RenderLabel(ctx, label))
|
|
||||||
}
|
|
||||||
htmlCode += "</span>"
|
|
||||||
return template.HTML(htmlCode)
|
|
||||||
},
|
|
||||||
"MermaidMaxSourceCharacters": func() int {
|
|
||||||
return setting.MermaidMaxSourceCharacters
|
|
||||||
},
|
|
||||||
"Join": strings.Join,
|
|
||||||
"QueryEscape": url.QueryEscape,
|
|
||||||
"DotEscape": DotEscape,
|
|
||||||
"Iterate": func(arg interface{}) (items []int64) {
|
|
||||||
count, _ := util.ToInt64(arg)
|
|
||||||
for i := int64(0); i < count; i++ {
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
},
|
|
||||||
"HasPrefix": strings.HasPrefix,
|
|
||||||
"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
|
"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
|
||||||
var curBranch string
|
var curBranch string
|
||||||
if repo.ID != baseRepo.ID {
|
if repo.ID != baseRepo.ID {
|
||||||
|
@ -340,45 +363,6 @@ func NewFuncMap() []template.FuncMap {
|
||||||
curBranch,
|
curBranch,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"Eval": Eval,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTextFuncMap returns functions for injecting to text templates
|
|
||||||
// It's a subset of those used for HTML and other templates
|
|
||||||
func NewTextFuncMap() []texttmpl.FuncMap {
|
|
||||||
return []texttmpl.FuncMap{map[string]interface{}{
|
|
||||||
"AppName": func() string {
|
|
||||||
return setting.AppName
|
|
||||||
},
|
|
||||||
"AppSubUrl": func() string {
|
|
||||||
return setting.AppSubURL
|
|
||||||
},
|
|
||||||
"AppUrl": func() string {
|
|
||||||
return setting.AppURL
|
|
||||||
},
|
|
||||||
"AppVer": func() string {
|
|
||||||
return setting.AppVer
|
|
||||||
},
|
|
||||||
"AppDomain": func() string { // documented in mail-templates.md
|
|
||||||
return setting.Domain
|
|
||||||
},
|
|
||||||
"TimeSince": timeutil.TimeSince,
|
|
||||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
|
||||||
"DateFmtLong": func(t time.Time) string {
|
|
||||||
return t.Format(time.RFC1123Z)
|
|
||||||
},
|
|
||||||
"EllipsisString": base.EllipsisString,
|
|
||||||
"URLJoin": util.URLJoin,
|
|
||||||
"Printf": fmt.Sprintf,
|
|
||||||
"Escape": Escape,
|
|
||||||
"Sec2Time": util.SecToTime,
|
|
||||||
"ParseDeadline": func(deadline string) []string {
|
|
||||||
return strings.Split(deadline, "|")
|
|
||||||
},
|
|
||||||
"dict": dict,
|
|
||||||
"QueryEscape": url.QueryEscape,
|
|
||||||
"Eval": Eval,
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,16 +441,6 @@ func Str2html(raw string) template.HTML {
|
||||||
return template.HTML(markup.Sanitize(raw))
|
return template.HTML(markup.Sanitize(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape escapes a HTML string
|
|
||||||
func Escape(raw string) string {
|
|
||||||
return html.EscapeString(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSEscape escapes a JS string
|
|
||||||
func JSEscape(raw string) string {
|
|
||||||
return template.JSEscapeString(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
|
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
|
||||||
func DotEscape(raw string) string {
|
func DotEscape(raw string) string {
|
||||||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
||||||
|
@ -771,25 +745,6 @@ func MigrationIcon(hostname string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
|
||||||
// Split template into subject and body
|
|
||||||
var subjectContent []byte
|
|
||||||
bodyContent := content
|
|
||||||
loc := mailSubjectSplit.FindIndex(content)
|
|
||||||
if loc != nil {
|
|
||||||
subjectContent = content[0:loc[0]]
|
|
||||||
bodyContent = content[loc[1]:]
|
|
||||||
}
|
|
||||||
if _, err := stpl.New(name).
|
|
||||||
Parse(string(subjectContent)); err != nil {
|
|
||||||
log.Warn("Failed to parse template [%s/subject]: %v", name, err)
|
|
||||||
}
|
|
||||||
if _, err := btpl.New(name).
|
|
||||||
Parse(string(bodyContent)); err != nil {
|
|
||||||
log.Warn("Failed to parse template [%s/body]: %v", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteAddress struct {
|
type remoteAddress struct {
|
||||||
Address string
|
Address string
|
||||||
Username string
|
Username string
|
||||||
|
|
|
@ -6,6 +6,7 @@ package templates
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
@ -15,9 +16,11 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
texttemplate "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/watcher"
|
"code.gitea.io/gitea/modules/watcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,6 +37,8 @@ type HTMLRender struct {
|
||||||
templates atomic.Pointer[template.Template]
|
templates atomic.Pointer[template.Template]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
||||||
|
|
||||||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
|
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
|
||||||
if respWriter, ok := w.(http.ResponseWriter); ok {
|
if respWriter, ok := w.(http.ResponseWriter); ok {
|
||||||
if respWriter.Header().Get("Content-Type") == "" {
|
if respWriter.Header().Get("Content-Type") == "" {
|
||||||
|
@ -41,11 +46,23 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}
|
||||||
}
|
}
|
||||||
respWriter.WriteHeader(status)
|
respWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
return h.templates.Load().ExecuteTemplate(w, name, data)
|
t, err := h.TemplateLookup(name)
|
||||||
|
if err != nil {
|
||||||
|
return texttemplate.ExecError{Name: name, Err: err}
|
||||||
|
}
|
||||||
|
return t.Execute(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTMLRender) TemplateLookup(t string) *template.Template {
|
func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
|
||||||
return h.templates.Load().Lookup(t)
|
tmpls := h.templates.Load()
|
||||||
|
if tmpls == nil {
|
||||||
|
return nil, ErrTemplateNotInitialized
|
||||||
|
}
|
||||||
|
tmpl := tmpls.Lookup(name)
|
||||||
|
if tmpl == nil {
|
||||||
|
return nil, util.ErrNotExist
|
||||||
|
}
|
||||||
|
return tmpl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTMLRender) CompileTemplates() error {
|
func (h *HTMLRender) CompileTemplates() error {
|
||||||
|
@ -237,6 +254,12 @@ func GetLineFromTemplate(templateName string, targetLineNum int, target string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: this algorithm could provide incorrect results and mislead the developers.
|
||||||
|
// For example: Undefined function "file" in template .....
|
||||||
|
// {{Func .file.Addition file.Deletion .file.Addition}}
|
||||||
|
// ^^^^ ^(the real error is here)
|
||||||
|
// The pointer is added to the first one, but the second one is the real incorrect one.
|
||||||
|
//
|
||||||
// If there is a provided target to look for in the line add a pointer to it
|
// If there is a provided target to look for in the line add a pointer to it
|
||||||
// e.g. ^^^^^^^
|
// e.g. ^^^^^^^
|
||||||
if target != "" {
|
if target != "" {
|
||||||
|
|
|
@ -11,16 +11,53 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
texttmpl "text/template"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/watcher"
|
"code.gitea.io/gitea/modules/watcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||||
|
func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||||
|
return texttmpl.FuncMap{
|
||||||
|
"dict": dict,
|
||||||
|
"Eval": Eval,
|
||||||
|
|
||||||
|
"EllipsisString": base.EllipsisString,
|
||||||
|
"AppName": func() string {
|
||||||
|
return setting.AppName
|
||||||
|
},
|
||||||
|
"AppDomain": func() string { // documented in mail-templates.md
|
||||||
|
return setting.Domain
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
|
||||||
|
// Split template into subject and body
|
||||||
|
var subjectContent []byte
|
||||||
|
bodyContent := content
|
||||||
|
loc := mailSubjectSplit.FindIndex(content)
|
||||||
|
if loc != nil {
|
||||||
|
subjectContent = content[0:loc[0]]
|
||||||
|
bodyContent = content[loc[1]:]
|
||||||
|
}
|
||||||
|
if _, err := stpl.New(name).
|
||||||
|
Parse(string(subjectContent)); err != nil {
|
||||||
|
log.Warn("Failed to parse template [%s/subject]: %v", name, err)
|
||||||
|
}
|
||||||
|
if _, err := btpl.New(name).
|
||||||
|
Parse(string(bodyContent)); err != nil {
|
||||||
|
log.Warn("Failed to parse template [%s/body]: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mailer provides the templates required for sending notification mails.
|
// Mailer provides the templates required for sending notification mails.
|
||||||
func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||||
for _, funcs := range NewTextFuncMap() {
|
subjectTemplates := texttmpl.New("")
|
||||||
subjectTemplates.Funcs(funcs)
|
bodyTemplates := template.New("")
|
||||||
}
|
|
||||||
|
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||||
for _, funcs := range NewFuncMap() {
|
for _, funcs := range NewFuncMap() {
|
||||||
bodyTemplates.Funcs(funcs)
|
bodyTemplates.Funcs(funcs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,8 +133,8 @@ func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error
|
||||||
|
|
||||||
type mockRender struct{}
|
type mockRender struct{}
|
||||||
|
|
||||||
func (tr *mockRender) TemplateLookup(tmpl string) *template.Template {
|
func (tr *mockRender) TemplateLookup(tmpl string) (*template.Template, error) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ interface{}) error {
|
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ interface{}) error {
|
||||||
|
|
|
@ -578,12 +578,15 @@ func GrantApplicationOAuth(ctx *context.Context) {
|
||||||
|
|
||||||
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
|
||||||
func OIDCWellKnown(ctx *context.Context) {
|
func OIDCWellKnown(ctx *context.Context) {
|
||||||
t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
|
t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("unable to find template", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||||
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
|
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
|
||||||
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
|
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
||||||
log.Error("%v", err)
|
ctx.ServerError("unable to execute template", err)
|
||||||
ctx.Error(http.StatusInternalServerError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,8 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// tplSwaggerV1Json swagger v1 json template
|
// tplSwaggerV1Json swagger v1 json template
|
||||||
|
@ -16,10 +13,13 @@ const tplSwaggerV1Json base.TplName = "swagger/v1_json"
|
||||||
|
|
||||||
// SwaggerV1Json render swagger v1 json
|
// SwaggerV1Json render swagger v1 json
|
||||||
func SwaggerV1Json(ctx *context.Context) {
|
func SwaggerV1Json(ctx *context.Context) {
|
||||||
t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
|
t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("unable to find template", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||||
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
|
if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
|
||||||
log.Error("%v", err)
|
ctx.ServerError("unable to execute template", err)
|
||||||
ctx.Error(http.StatusInternalServerError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
{{$reactions := .Reactions.GroupByType}}
|
{{$reactions := .Reactions.GroupByType}}
|
||||||
{{if $reactions}}
|
{{if $reactions}}
|
||||||
<div class="ui attached segment reactions">
|
<div class="ui attached segment reactions">
|
||||||
{{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
|
{{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{Eval .file.Addition "+" .file.Deletion}}
|
{{Eval .file.Addition "+" .file.Deletion}}
|
||||||
<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{.root.locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
|
<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{.root.locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
|
||||||
<div class="diff-stats-add-bar" style="width: {{DiffStatsWidth .file.Addition .file.Deletion}}%"></div>
|
{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
|
||||||
|
<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
|
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
{{$reactions := .Issue.Reactions.GroupByType}}
|
{{$reactions := .Issue.Reactions.GroupByType}}
|
||||||
{{if $reactions}}
|
{{if $reactions}}
|
||||||
<div class="ui attached segment reactions" role="note">
|
<div class="ui attached segment reactions" role="note">
|
||||||
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}}
|
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
{{$reactions := .Reactions.GroupByType}}
|
{{$reactions := .Reactions.GroupByType}}
|
||||||
{{if $reactions}}
|
{{if $reactions}}
|
||||||
<div class="ui attached segment reactions" role="note">
|
<div class="ui attached segment reactions" role="note">
|
||||||
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -436,7 +436,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -458,7 +458,7 @@
|
||||||
{{$reactions := .Reactions.GroupByType}}
|
{{$reactions := .Reactions.GroupByType}}
|
||||||
{{if $reactions}}
|
{{if $reactions}}
|
||||||
<div class="ui attached segment reactions">
|
<div class="ui attached segment reactions">
|
||||||
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -563,7 +563,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not $.Repository.IsArchived}}
|
{{if not $.Repository.IsArchived}}
|
||||||
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
|
||||||
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -582,7 +582,7 @@
|
||||||
{{$reactions := .Reactions.GroupByType}}
|
{{$reactions := .Reactions.GroupByType}}
|
||||||
{{if $reactions}}
|
{{if $reactions}}
|
||||||
<div class="ui attached segment reactions">
|
<div class="ui attached segment reactions">
|
||||||
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{$referenceUrl := ""}}
|
{{$referenceUrl := ""}}
|
||||||
{{if .issue}}
|
{{if .issue}}
|
||||||
{{$referenceUrl = Printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
|
{{$referenceUrl = printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{$referenceUrl = Printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
|
{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}</div>
|
<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}</div>
|
||||||
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}</div>
|
<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{.locale.Tr "repo.issues.context.reference_issue"}}
|
{{.locale.Tr "repo.issues.context.reference_issue"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="content" style="text-align:left">
|
<div class="content" style="text-align:left">
|
||||||
<form class="ui form" action="{{Printf "%s/issues/new" .Repository.Link}}" method="post">
|
<form class="ui form" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="ui segment content">
|
<div class="ui segment content">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
<div class="ui three stackable cards">
|
<div class="ui three stackable cards">
|
||||||
{{range .Services}}
|
{{range .Services}}
|
||||||
<a class="ui card gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
|
<a class="ui card gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
|
||||||
{{svg (Printf "gitea-%s" .Name) 184}}
|
{{svg (printf "gitea-%s" .Name) 184}}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header gt-tc">
|
<div class="header gt-tc">
|
||||||
{{.Title}}
|
{{.Title}}
|
||||||
</div>
|
</div>
|
||||||
<div class="description gt-tc">
|
<div class="description gt-tc">
|
||||||
{{(Printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
|
{{(printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
Loading…
Reference in New Issue