Make PR form use toast to show error message (#29545)

![image](https://github.com/go-gitea/gitea/assets/2114189/b7a14ed6-db89-4f21-a590-66cd33307233)

(cherry picked from commit 27deea7330f83ddb37c918afbb4159053d8847cb)
This commit is contained in:
wxiaoguang 2024-03-02 23:05:07 +08:00 committed by Earl Warren
parent d509031e84
commit 221a28436a
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
7 changed files with 35 additions and 30 deletions

View file

@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) {
if len(e.Message) == 0 { if len(e.Message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message")) ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"), "Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(e.Message), "Details": utils.SanitizeFlashErrorString(e.Message),

View file

@ -382,7 +382,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
if len(errPushRej.Message) == 0 { if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"), "Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message), "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@ -394,7 +394,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.RenderWithErr(flashError, tplEditFile, &form) ctx.RenderWithErr(flashError, tplEditFile, &form)
} }
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
"Details": utils.SanitizeFlashErrorString(err.Error()), "Details": utils.SanitizeFlashErrorString(err.Error()),
@ -590,7 +590,7 @@ func DeleteFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 { if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"), "Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message), "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@ -797,7 +797,7 @@ func UploadFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 { if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"), "Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message), "Details": utils.SanitizeFlashErrorString(errPushRej.Message),

View file

@ -9,6 +9,7 @@ import (
stdCtx "context" stdCtx "context"
"errors" "errors"
"fmt" "fmt"
"html/template"
"math/big" "math/big"
"net/http" "net/http"
"net/url" "net/url"
@ -1022,7 +1023,7 @@ func NewIssue(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplIssueNew) ctx.HTML(http.StatusOK, tplIssueNew)
} }
func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string { func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
var files []string var files []string
for k := range errs { for k := range errs {
files = append(files, k) files = append(files, k)
@ -1034,14 +1035,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file])) lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
} }
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
}) })
if err != nil { if err != nil {
log.Debug("render flash error: %v", err) log.Debug("render flash error: %v", err)
flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates") flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
} }
return flashError return flashError
} }
@ -3324,7 +3325,7 @@ func ChangeIssueReaction(ctx *context.Context) {
return return
} }
html, err := ctx.RenderToString(tplReactions, map[string]any{ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data, "ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
"Reactions": issue.Reactions.GroupByType(), "Reactions": issue.Reactions.GroupByType(),
@ -3431,7 +3432,7 @@ func ChangeCommentReaction(ctx *context.Context) {
return return
} }
html, err := ctx.RenderToString(tplReactions, map[string]any{ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data, "ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
"Reactions": comment.Reactions.GroupByType(), "Reactions": comment.Reactions.GroupByType(),
@ -3574,8 +3575,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
return err return err
} }
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string { func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{ attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
"ctxData": ctx.Data, "ctxData": ctx.Data,
"Attachments": attachments, "Attachments": attachments,
"Content": content, "Content": content,

View file

@ -1159,7 +1159,7 @@ func UpdatePullRequest(ctx *context.Context) {
if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil { if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if models.IsErrMergeConflicts(err) { if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts) conflictError := err.(models.ErrMergeConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.merge_conflict"), "Message": ctx.Tr("repo.pulls.merge_conflict"),
"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@ -1173,7 +1173,7 @@ func UpdatePullRequest(ctx *context.Context) {
return return
} else if models.IsErrRebaseConflicts(err) { } else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts) conflictError := err.(models.ErrRebaseConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@ -1305,7 +1305,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if models.IsErrMergeConflicts(err) { } else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts) conflictError := err.(models.ErrMergeConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.merge_conflict"), "Message": ctx.Tr("repo.editor.merge_conflict"),
"Summary": ctx.Tr("repo.editor.merge_conflict_summary"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@ -1318,7 +1318,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONRedirect(issue.Link()) ctx.JSONRedirect(issue.Link())
} else if models.IsErrRebaseConflicts(err) { } else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts) conflictError := err.(models.ErrRebaseConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@ -1348,7 +1348,7 @@ func MergePullRequest(ctx *context.Context) {
if len(message) == 0 { if len(message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message")) ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
} else { } else {
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"), "Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message), "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@ -1525,7 +1525,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message")) ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
return return
} }
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"), "Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message), "Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@ -1534,8 +1534,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.ServerError("CompareAndPullRequest.HTMLString", err) ctx.ServerError("CompareAndPullRequest.HTMLString", err)
return return
} }
ctx.Flash.Error(flashError) ctx.JSONError(flashError)
ctx.JSONRedirect(ctx.Link + "?" + ctx.Req.URL.RawQuery) // FIXME: it's unfriendly, and will make the content lost
return return
} }
ctx.ServerError("NewPullRequest", err) ctx.ServerError("NewPullRequest", err)

View file

@ -6,6 +6,7 @@ package context
import ( import (
"errors" "errors"
"fmt" "fmt"
"html/template"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -104,11 +105,11 @@ func (ctx *Context) JSONTemplate(tmpl base.TplName) {
} }
} }
// RenderToString renders the template content to a string // RenderToHTML renders the template content to a HTML string
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
var buf strings.Builder var buf strings.Builder
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext) err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
return buf.String(), err return template.HTML(buf.String()), err
} }
// RenderWithErr used for page has form validation but need to prompt error to users. // RenderWithErr used for page has form validation but need to prompt error to users.

View file

@ -91,19 +91,24 @@ async function fetchActionDoRequest(actionElem, url, opt) {
} else { } else {
window.location.reload(); window.location.reload();
} }
return;
} else if (resp.status >= 400 && resp.status < 500) { } else if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json(); const data = await resp.json();
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
showErrorToast(data.errorMessage || `server error: ${resp.status}`); if (data.errorMessage) {
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
} else {
showErrorToast(`server error: ${resp.status}`);
}
} else { } else {
showErrorToast(`server error: ${resp.status}`); showErrorToast(`server error: ${resp.status}`);
} }
} catch (e) { } catch (e) {
console.error('error when doRequest', e); console.error('error when doRequest', e);
actionElem.classList.remove('is-loading', 'small-loading-icon'); showErrorToast(`${i18n.network_error} ${e}`);
showErrorToast(i18n.network_error);
} }
actionElem.classList.remove('is-loading', 'small-loading-icon');
} }
async function formFetchAction(e) { async function formFetchAction(e) {

View file

@ -21,13 +21,12 @@ const levels = {
}; };
// See https://github.com/apvarun/toastify-js#api for options // See https://github.com/apvarun/toastify-js#api for options
function showToast(message, level, {gravity, position, duration, ...other} = {}) { function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
const {icon, background, duration: levelDuration} = levels[level ?? 'info']; const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({ const toast = Toastify({
text: ` text: `
<div class='toast-icon'>${svg(icon)}</div> <div class='toast-icon'>${svg(icon)}</div>
<div class='toast-body'>${htmlEscape(message)}</div> <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
<button class='toast-close'>${svg('octicon-x')}</button> <button class='toast-close'>${svg('octicon-x')}</button>
`, `,
escapeMarkup: false, escapeMarkup: false,