mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	Make PR form use toast to show error message (#29545)

This commit is contained in:
		| @@ -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), | ||||||
|   | |||||||
| @@ -341,7 +341,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), | ||||||
| @@ -353,7 +353,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()), | ||||||
| @@ -542,7 +542,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), | ||||||
| @@ -742,7 +742,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), | ||||||
|   | |||||||
| @@ -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" | ||||||
| @@ -1016,7 +1017,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) | ||||||
| @@ -1028,14 +1029,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 | ||||||
| } | } | ||||||
| @@ -3296,7 +3297,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(), | ||||||
| @@ -3403,7 +3404,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(), | ||||||
| @@ -3546,8 +3547,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, | ||||||
|   | |||||||
| @@ -1129,7 +1129,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), | ||||||
| @@ -1143,7 +1143,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), | ||||||
| @@ -1275,7 +1275,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), | ||||||
| @@ -1288,7 +1288,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), | ||||||
| @@ -1318,7 +1318,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), | ||||||
| @@ -1491,7 +1491,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), | ||||||
| @@ -1500,8 +1500,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) | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
| @@ -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) { | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user