diff --git a/models/issues/label.go b/models/issues/label.go index f6ecc68cd1..2397a29e35 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -116,12 +116,17 @@ func (l *Label) CalOpenIssues() { func (l *Label) SetArchived(isArchived bool) { if !isArchived { l.ArchivedUnix = timeutil.TimeStamp(0) - } else if isArchived && l.ArchivedUnix.IsZero() { + } else if isArchived && !l.IsArchived() { // Only change the date when it is newly archived. l.ArchivedUnix = timeutil.TimeStampNow() } } +// IsArchived returns true if label is an archived +func (l *Label) IsArchived() bool { + return !l.ArchivedUnix.IsZero() +} + // CalOpenOrgIssues calculates the open issues of a label for a specific repo func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ @@ -166,11 +171,6 @@ func (l *Label) BelongsToOrg() bool { return l.OrgID > 0 } -// IsArchived returns true if label is an archived -func (l *Label) IsArchived() bool { - return l.ArchivedUnix > 0 -} - // BelongsToRepo returns true if label is a repository label func (l *Label) BelongsToRepo() bool { return l.RepoID > 0 diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 5deb051abe..f593e1b897 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" ) @@ -104,6 +105,18 @@ func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { return template.HTML(htmlWithCodeTags) } +const ( + activeLabelOpacity = uint8(255) + archivedLabelOpacity = uint8(127) +) + +func GetLabelOpacityByte(isArchived bool) uint8 { + if isArchived { + return archivedLabelOpacity + } + return activeLabelOpacity +} + // RenderIssueTitle renders issue/pull title with defined post processors func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML { renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ @@ -118,22 +131,34 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) } // RenderLabel renders a label -func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML { - labelScope := label.ExclusiveScope() - - textColor := "#111" +// 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 { + var ( + archivedCSSClass string + textColor = "#111" + labelScope = label.ExclusiveScope() + ) r, g, b := util.HexToRBGColor(label.Color) + // Determine if label text should be light or dark to be readable on background color + // this doesn't account for saturation or transparency if util.UseLightTextOnBackground(r, g, b) { textColor = "#eee" } description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) + if label.IsArchived() { + archivedCSSClass = "archived-label" + description = locale.TrString("repo.issues.archived_label_description", description) + } + if labelScope == "" { // Regular label - s := fmt.Sprintf("
%s
", - textColor, label.Color, description, RenderEmoji(ctx, label.Name)) + + labelColor := label.Color + hex.EncodeToString([]byte{GetLabelOpacityByte(label.IsArchived())}) + s := fmt.Sprintf("
%s
", + archivedCSSClass, textColor, labelColor, description, RenderEmoji(ctx, label.Name)) return template.HTML(s) } @@ -152,25 +177,28 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML { darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) + opacity := GetLabelOpacityByte(label.IsArchived()) scopeBytes := []byte{ uint8(math.Min(math.Round(r*darkenFactor), 255)), uint8(math.Min(math.Round(g*darkenFactor), 255)), uint8(math.Min(math.Round(b*darkenFactor), 255)), + opacity, } itemBytes := []byte{ uint8(math.Min(math.Round(r*lightenFactor), 255)), uint8(math.Min(math.Round(g*lightenFactor), 255)), uint8(math.Min(math.Round(b*lightenFactor), 255)), + opacity, } - itemColor := "#" + hex.EncodeToString(itemBytes) scopeColor := "#" + hex.EncodeToString(scopeBytes) + itemColor := "#" + hex.EncodeToString(itemBytes) - s := fmt.Sprintf(""+ + s := fmt.Sprintf(""+ "
%s
"+ "
%s
"+ "
", - description, + archivedCSSClass, description, textColor, scopeColor, scopeText, textColor, itemColor, itemText) return template.HTML(s) @@ -211,7 +239,7 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n return output } -func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML { +func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string) template.HTML { htmlCode := `` for _, label := range labels { // Protect against nil value in labels - shouldn't happen but would cause a panic if so @@ -219,7 +247,7 @@ func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink st continue } htmlCode += fmt.Sprintf("%s ", - repoLink, label.ID, RenderLabel(ctx, label)) + repoLink, label.ID, RenderLabel(ctx, locale, label)) } htmlCode += "" return template.HTML(htmlCode) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e595f40269..3a871b2eb8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1628,6 +1628,7 @@ issues.label_modify = Edit label issues.label_deletion = Delete label issues.label_deletion_desc = Deleting a label removes it from all issues. Continue? issues.label_deletion_success = The label has been deleted. +issues.archived_label_description = (Archived) %s issues.label.filter_sort.alphabetically = Alphabetically issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically issues.label.filter_sort.by_size = Smallest size diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 9dedaefa4b..81bee4dbb5 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -112,12 +111,11 @@ func NewLabel(ctx *context.Context) { } l := &issues_model.Label{ - RepoID: ctx.Repo.Repository.ID, - Name: form.Title, - Exclusive: form.Exclusive, - Description: form.Description, - Color: form.Color, - ArchivedUnix: timeutil.TimeStamp(0), + RepoID: ctx.Repo.Repository.ID, + Name: form.Title, + Exclusive: form.Exclusive, + Description: form.Description, + Color: form.Color, } if err := issues_model.NewLabel(ctx, l); err != nil { ctx.ServerError("NewLabel", err) diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index f3d533b862..bb9340bb2e 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -61,7 +61,7 @@ {{if or .Labels .Assignees}}
{{range .Labels}} - {{RenderLabel ctx .}} + {{RenderLabel ctx ctx.Locale .}} {{end}}
{{range .Assignees}} diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 3b770ac01d..d559f335b7 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -30,7 +30,7 @@ {{end}} {{$previousExclusiveScope = $exclusiveScope}}
- {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}} + {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context ctx.Locale .}} {{template "repo/issue/labels/label_archived" .}}
{{end}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 6bf6d6b9d3..997557c45e 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -42,7 +42,7 @@ {{svg "octicon-check"}} {{end}} {{end}} - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}}

{{template "repo/issue/labels/label_archived" .}}

{{end}} diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl index 2480115129..3651ba118f 100644 --- a/templates/repo/issue/labels/label.tmpl +++ b/templates/repo/issue/labels/label.tmpl @@ -3,5 +3,5 @@ id="label_{{.label.ID}}" href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}} > - {{- RenderLabel $.Context .label -}} + {{- RenderLabel $.Context ctx.Locale .label -}} diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index ad4d8697e7..d84f14242a 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -32,7 +32,7 @@ {{range .Labels}}
  • - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}
    @@ -72,7 +72,7 @@ {{range .OrgLabels}}
  • - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}
    diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl index 067361bf1e..e5f15caca5 100644 --- a/templates/repo/issue/labels/labels_selector_field.tmpl +++ b/templates/repo/issue/labels/labels_selector_field.tmpl @@ -21,7 +21,7 @@
    {{end}} {{$previousExclusiveScope = $exclusiveScope}} - {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}

    {{template "repo/issue/labels/label_archived" .}}

    @@ -34,7 +34,7 @@
    {{end}} {{$previousExclusiveScope = $exclusiveScope}} - {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context .}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}
    {{.Description | RenderEmoji $.Context}}{{end}}

    {{template "repo/issue/labels/label_archived" .}}

    diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 162762950d..a5fd02c190 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -177,11 +177,11 @@ {{template "shared/user/authorlink" .Poster}} {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) $createdStr}} {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}} {{end}}
    diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 06887c2914..1c0dfcc551 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -21,7 +21,7 @@ {{end}} {{range .Labels}} - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{end}}
  • diff --git a/tests/integration/archived_labels_display_test.go b/tests/integration/archived_labels_display_test.go new file mode 100644 index 0000000000..c9748f81d6 --- /dev/null +++ b/tests/integration/archived_labels_display_test.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +func TestArchivedLabelVisualProperties(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") + + // Create labels + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/new", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "title": "active_label", + "description": "", + "color": "#aa00aa", + }), http.StatusSeeOther) + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/new", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "title": "archived_label", + "description": "", + "color": "#00aa00", + }), http.StatusSeeOther) + + // Get ID of label to archive it + var id string + doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "user2/repo1/labels"), http.StatusOK).Body) + doc.Find(".issue-label-list .item").Each(func(i int, s *goquery.Selection) { + label := s.Find(".label-title .label") + if label.Text() == "archived_label" { + href, _ := s.Find(".label-issues a.open-issues").Attr("href") + hrefParts := strings.Split(href, "=") + id = hrefParts[len(hrefParts)-1] + } + }) + + // Make label archived + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/edit", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "id": id, + "title": "archived_label", + "is_archived": "on", + "description": "", + "color": "#00aa00", + }), http.StatusSeeOther) + + // Test label properties + doc = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "user2/repo1/labels"), http.StatusOK).Body) + doc.Find(".issue-label-list .item").Each(func(i int, s *goquery.Selection) { + label := s.Find(".label-title .label") + style, _ := label.Attr("style") + + if label.Text() == "active_label" { + assert.False(t, label.HasClass("archived-label")) + assert.Contains(t, style, "background-color: #aa00aaff") + } else if label.Text() == "archived_label" { + assert.True(t, label.HasClass("archived-label")) + assert.Contains(t, style, "background-color: #00aa007f") + } + }) + }) +} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 03e4f1d74b..565645bc7b 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2435,6 +2435,10 @@ margin-left: 0; } +.archived-label { + filter: grayscale(0.25) saturate(0.75); +} + .repo-button-row { margin: 10px 0; display: flex;