mirror of
				https://github.com/go-gitea/gitea
				synced 2025-09-28 03:28:13 +00:00 
			
		
		
		
	Backport #30763 by wxiaoguang Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -121,29 +121,25 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) | |||||||
| // RenderLabel renders a label | // RenderLabel renders a label | ||||||
| // locale is needed due to an import cycle with our context providing the `Tr` function | // locale is needed due to an import cycle with our context providing the `Tr` function | ||||||
| func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { | func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { | ||||||
| 	var ( | 	var extraCSSClasses string | ||||||
| 		archivedCSSClass string | 	textColor := util.ContrastColor(label.Color) | ||||||
| 		textColor        = util.ContrastColor(label.Color) | 	labelScope := label.ExclusiveScope() | ||||||
| 		labelScope       = label.ExclusiveScope() | 	descriptionText := emoji.ReplaceAliases(label.Description) | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) |  | ||||||
|  |  | ||||||
| 	if label.IsArchived() { | 	if label.IsArchived() { | ||||||
| 		archivedCSSClass = "archived-label" | 		extraCSSClasses = "archived-label" | ||||||
| 		description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description) | 		descriptionText = fmt.Sprintf("(%s) %s", locale.TrString("archived"), descriptionText) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if labelScope == "" { | 	if labelScope == "" { | ||||||
| 		// Regular label | 		// Regular label | ||||||
| 		s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>", | 		return HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`, | ||||||
| 			archivedCSSClass, textColor, label.Color, description, RenderEmoji(ctx, label.Name)) | 			extraCSSClasses, textColor, label.Color, descriptionText, RenderEmoji(ctx, label.Name)) | ||||||
| 		return template.HTML(s) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Scoped label | 	// Scoped label | ||||||
| 	scopeText := RenderEmoji(ctx, labelScope) | 	scopeHTML := RenderEmoji(ctx, labelScope) | ||||||
| 	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) | 	itemHTML := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) | ||||||
|  |  | ||||||
| 	// Make scope and item background colors slightly darker and lighter respectively. | 	// Make scope and item background colors slightly darker and lighter respectively. | ||||||
| 	// More contrast needed with higher luminance, empirically tweaked. | 	// More contrast needed with higher luminance, empirically tweaked. | ||||||
| @@ -171,14 +167,13 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m | |||||||
| 	itemColor := "#" + hex.EncodeToString(itemBytes) | 	itemColor := "#" + hex.EncodeToString(itemBytes) | ||||||
| 	scopeColor := "#" + hex.EncodeToString(scopeBytes) | 	scopeColor := "#" + hex.EncodeToString(scopeBytes) | ||||||
|  |  | ||||||
| 	s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+ | 	return HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+ | ||||||
| 		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+ | 		`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+ | ||||||
| 		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+ | 		`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+ | ||||||
| 		"</span>", | 		`</span>`, | ||||||
| 		archivedCSSClass, description, | 		extraCSSClasses, descriptionText, | ||||||
| 		textColor, scopeColor, scopeText, | 		textColor, scopeColor, scopeHTML, | ||||||
| 		textColor, itemColor, itemText) | 		textColor, itemColor, itemHTML) | ||||||
| 	return template.HTML(s) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // RenderEmoji renders html text with emoji post processors | // RenderEmoji renders html text with emoji post processors | ||||||
|   | |||||||
| @@ -2177,7 +2177,10 @@ func GetIssueInfo(ctx *context.Context) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue)) | 	ctx.JSON(http.StatusOK, map[string]any{ | ||||||
|  | 		"convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue), | ||||||
|  | 		"renderedLabels": templates.RenderLabels(ctx, ctx.Locale, issue.Labels, ctx.Repo.RepoLink, issue), | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // UpdateIssueTitle change issue's title | // UpdateIssueTitle change issue's title | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package integration | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| @@ -573,10 +574,14 @@ func TestGetIssueInfo(t *testing.T) { | |||||||
| 	urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index) | 	urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index) | ||||||
| 	req := NewRequest(t, "GET", urlStr) | 	req := NewRequest(t, "GET", urlStr) | ||||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||||
| 	var apiIssue api.Issue | 	var respStruct struct { | ||||||
| 	DecodeJSON(t, resp, &apiIssue) | 		ConvertedIssue api.Issue | ||||||
|  | 		RenderedLabels template.HTML | ||||||
|  | 	} | ||||||
|  | 	DecodeJSON(t, resp, &respStruct) | ||||||
|  |  | ||||||
| 	assert.EqualValues(t, issue.ID, apiIssue.ID) | 	assert.EqualValues(t, issue.ID, respStruct.ConvertedIssue.ID) | ||||||
|  | 	assert.Contains(t, string(respStruct.RenderedLabels), `"labels-list"`) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestUpdateIssueDeadline(t *testing.T) { | func TestUpdateIssueDeadline(t *testing.T) { | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import {SvgIcon} from '../svg.js'; | import {SvgIcon} from '../svg.js'; | ||||||
| import {contrastColor} from '../utils/color.js'; |  | ||||||
| import {GET} from '../modules/fetch.js'; | import {GET} from '../modules/fetch.js'; | ||||||
|  |  | ||||||
| const {appSubUrl, i18n} = window.config; | const {appSubUrl, i18n} = window.config; | ||||||
| @@ -10,6 +9,7 @@ export default { | |||||||
|   data: () => ({ |   data: () => ({ | ||||||
|     loading: false, |     loading: false, | ||||||
|     issue: null, |     issue: null, | ||||||
|  |     renderedLabels: '', | ||||||
|     i18nErrorOccurred: i18n.error_occurred, |     i18nErrorOccurred: i18n.error_occurred, | ||||||
|     i18nErrorMessage: null, |     i18nErrorMessage: null, | ||||||
|   }), |   }), | ||||||
| @@ -56,14 +56,6 @@ export default { | |||||||
|       } |       } | ||||||
|       return 'red'; // Closed Issue |       return 'red'; // Closed Issue | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     labels() { |  | ||||||
|       return this.issue.labels.map((label) => ({ |  | ||||||
|         name: label.name, |  | ||||||
|         color: `#${label.color}`, |  | ||||||
|         textColor: contrastColor(`#${label.color}`), |  | ||||||
|       })); |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.$refs.root.addEventListener('ce-load-context-popup', (e) => { |     this.$refs.root.addEventListener('ce-load-context-popup', (e) => { | ||||||
| @@ -79,13 +71,14 @@ export default { | |||||||
|       this.i18nErrorMessage = null; |       this.i18nErrorMessage = null; | ||||||
|  |  | ||||||
|       try { |       try { | ||||||
|         const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); |         const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo | ||||||
|         const respJson = await response.json(); |         const respJson = await response.json(); | ||||||
|         if (!response.ok) { |         if (!response.ok) { | ||||||
|           this.i18nErrorMessage = respJson.message ?? i18n.network_error; |           this.i18nErrorMessage = respJson.message ?? i18n.network_error; | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|         this.issue = respJson; |         this.issue = respJson.convertedIssue; | ||||||
|  |         this.renderedLabels = respJson.renderedLabels; | ||||||
|       } catch { |       } catch { | ||||||
|         this.i18nErrorMessage = i18n.network_error; |         this.i18nErrorMessage = i18n.network_error; | ||||||
|       } finally { |       } finally { | ||||||
| @@ -102,16 +95,8 @@ export default { | |||||||
|       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> |       <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> | ||||||
|       <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> |       <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> | ||||||
|       <p>{{ body }}</p> |       <p>{{ body }}</p> | ||||||
|       <div class="labels-list"> |       <!-- eslint-disable-next-line vue/no-v-html --> | ||||||
|         <div |       <div v-html="renderedLabels"/> | ||||||
|           v-for="label in labels" |  | ||||||
|           :key="label.name" |  | ||||||
|           class="ui label" |  | ||||||
|           :style="{ color: label.textColor, backgroundColor: label.color }" |  | ||||||
|         > |  | ||||||
|           {{ label.name }} |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |     </div> | ||||||
|     <div v-if="!loading && issue === null"> |     <div v-if="!loading && issue === null"> | ||||||
|       <p><small>{{ i18nErrorOccurred }}</small></p> |       <p><small>{{ i18nErrorOccurred }}</small></p> | ||||||
|   | |||||||
| @@ -53,7 +53,7 @@ export function initCommonIssueListQuickGoto() { | |||||||
|     // try to check whether the parsed goto link is valid |     // try to check whether the parsed goto link is valid | ||||||
|     let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText); |     let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText); | ||||||
|     if (targetUrl) { |     if (targetUrl) { | ||||||
|       const res = await GET(`${targetUrl}/info`); |       const res = await GET(`${targetUrl}/info`); // backend: GetIssueInfo, it only checks whether the issue exists by status code | ||||||
|       if (res.status !== 200) targetUrl = ''; |       if (res.status !== 200) targetUrl = ''; | ||||||
|     } |     } | ||||||
|     // if the input value has changed, then ignore the result |     // if the input value has changed, then ignore the result | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user