From 2be8d8226ed9da9340fed0dada5128ee7c644b95 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sun, 5 Feb 2023 05:06:05 +0100 Subject: [PATCH 01/10] Label rendering refactor for consistency and code simplification * Labels now consistently have the same shape, emojis and tooltips everywhere. This includes the label list and label assignment menus. * In label list, show description below label same as label menus. * Don't use exactly black/white text colors to look a bit nicer. * Simplify text color computation. There is no point computing luminance in linear color space, as this is a perceptual problem and sRGB is closer to perceptually linear. * Increase height of label assignment menus to show more labels. Showing only 3-4 labels at a time leads to a lot of scrolling. * Render all labels with a new RenderLabel template helper function. --- models/issues/label.go | 54 ++++++------------- models/issues/label_test.go | 7 ++- modules/templates/helper.go | 20 ++++++- templates/projects/view.tmpl | 2 +- templates/repo/issue/labels/label.tmpl | 6 +-- templates/repo/issue/labels/label_list.tmpl | 30 ++++------- templates/repo/issue/list.tmpl | 4 +- templates/repo/issue/milestone_issues.tmpl | 4 +- templates/repo/issue/new_form.tmpl | 4 +- .../repo/issue/view_content/sidebar.tmpl | 4 +- templates/repo/projects/view.tmpl | 2 +- templates/shared/issuelist.tmpl | 2 +- web_src/js/components/ContextPopup.vue | 26 ++++----- web_src/js/features/repo-projects.js | 23 +++----- web_src/less/_base.less | 7 +-- web_src/less/_repository.less | 14 ++--- 16 files changed, 85 insertions(+), 124 deletions(-) diff --git a/models/issues/label.go b/models/issues/label.go index dbb7a139effdf..305c7105bb22a 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -7,8 +7,6 @@ package issues import ( "context" "fmt" - "html/template" - "math" "regexp" "strconv" "strings" @@ -159,49 +157,31 @@ func (label *Label) BelongsToRepo() bool { return label.RepoID > 0 } -// SrgbToLinear converts a component of an sRGB color to its linear intensity -// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ) -func SrgbToLinear(color uint8) float64 { - flt := float64(color) / 255 - if flt <= 0.04045 { - return flt / 12.92 +// Get color as RGB values in 0..255 range +func (label *Label) ColorRGB() (float64, float64, float64, error) { + color, err := strconv.ParseUint(label.Color[1:], 16, 64) + if err != nil { + return 0, 0, 0, err } - return math.Pow((flt+0.055)/1.055, 2.4) -} -// Luminance returns the luminance of an sRGB color -func Luminance(color uint32) float64 { - r := SrgbToLinear(uint8(0xFF & (color >> 16))) - g := SrgbToLinear(uint8(0xFF & (color >> 8))) - b := SrgbToLinear(uint8(0xFF & color)) - - // luminance ratios for sRGB - return 0.2126*r + 0.7152*g + 0.0722*b + r := float64(uint8(0xFF & (uint32(color) >> 16))) + g := float64(uint8(0xFF & (uint32(color) >> 8))) + b := float64(uint8(0xFF & uint32(color))) + return r, g, b, nil } -// LuminanceThreshold is the luminance at which white and black appear to have the same contrast -// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05 -// i.e. math.Sqrt(1.05*0.05) - 0.05 -const LuminanceThreshold float64 = 0.179 - -// ForegroundColor calculates the text color for labels based -// on their background color. -func (label *Label) ForegroundColor() template.CSS { +// Determine if label text should be light or dark to be readable on +// background color. +func (label *Label) UseLightTextColor() bool { if strings.HasPrefix(label.Color, "#") { - if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil { - // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation - luminance := Luminance(uint32(color)) - - // prefer white or black based upon contrast - if luminance < LuminanceThreshold { - return template.CSS("#fff") - } - return template.CSS("#000") + if r, g, b, err := label.ColorRGB(); err == nil { + // sRGB color space luminance + luminance := (0.299*r + 0.587*g + 0.114*b) / 255 + return luminance < 0.35 } } - // default to black - return template.CSS("#000") + return false } // NewLabel creates a new label diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 239e328d475c4..a2f87f7339275 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -4,7 +4,6 @@ package issues_test import ( - "html/template" "testing" "code.gitea.io/gitea/models/db" @@ -25,13 +24,13 @@ func TestLabel_CalOpenIssues(t *testing.T) { assert.EqualValues(t, 2, label.NumOpenIssues) } -func TestLabel_ForegroundColor(t *testing.T) { +func TestLabel_TextColor(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - assert.Equal(t, template.CSS("#000"), label.ForegroundColor()) + assert.False(t, label.UseLightTextColor()) label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}) - assert.Equal(t, template.CSS("#fff"), label.ForegroundColor()) + assert.True(t, label.UseLightTextColor()) } func TestNewLabels(t *testing.T) { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a390d9459298d..b6e626a788e8c 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -379,6 +379,9 @@ func NewFuncMap() []template.FuncMap { // the table is NOT sorted with this header return "" }, + "RenderLabel": func(label *issues_model.Label) template.HTML { + return template.HTML(RenderLabel(label)) + }, "RenderLabels": func(labels []*issues_model.Label, repoLink string) template.HTML { htmlCode := `` for _, label := range labels { @@ -386,8 +389,8 @@ func NewFuncMap() []template.FuncMap { if label == nil { continue } - htmlCode += fmt.Sprintf("%s ", - repoLink, label.ID, label.ForegroundColor(), label.Color, html.EscapeString(label.Description), RenderEmoji(label.Name)) + htmlCode += fmt.Sprintf("%s ", + repoLink, label.ID, RenderLabel(label)) } htmlCode += "" return template.HTML(htmlCode) @@ -798,6 +801,19 @@ func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[str return template.HTML(renderedText) } +// RenderLabel renders a label +func RenderLabel(label *issues_model.Label) string { + textColor := "#111" + if label.UseLightTextColor() { + textColor = "#eee" + } + + description := emoji.ReplaceAliases(label.Description) + + return fmt.Sprintf("
%s
", + textColor, label.Color, description, RenderEmoji(label.Name)) +} + // RenderEmoji renders html text with emoji post processors func RenderEmoji(text string) template.HTML { renderedText, err := markup.RenderEmoji(template.HTMLEscapeString(text)) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 42eb578074d83..16f03c304d0ff 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -234,7 +234,7 @@ {{if or .Labels .Assignees}}
{{range .Labels}} - {{.Name | RenderEmoji}} + {{RenderLabel .}} {{end}}
{{range .Assignees}} diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl index 0afe5cb6e7b1e..87d8f0c41c2c3 100644 --- a/templates/repo/issue/labels/label.tmpl +++ b/templates/repo/issue/labels/label.tmpl @@ -1,9 +1,7 @@ - {{.label.Name | RenderEmoji}} + {{RenderLabel .label}} diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 464c9fe208d58..7b9935ebbab71 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -30,19 +30,15 @@ {{range .Labels}}
  • -
    -
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    -
    -
  • -
    -
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    +
    + {{RenderLabel .}} + {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    -
    -
    - {{.Description | RenderEmoji}} -
    -
    -
    @@ -219,7 +219,7 @@ diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index f0d6ae6bfe98f..edaf0c84b012c 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -58,7 +58,7 @@ {{.locale.Tr "repo.issues.filter_label_exclude" | Safe}} {{.locale.Tr "repo.issues.filter_label_no_select"}} {{range .Labels}} - {{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{.Name | RenderEmoji}} + {{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if contain $.SelLabelIDs .ID}}{{svg "octicon-check"}}{{end}} {{RenderLabel .}} {{end}}
  • @@ -161,7 +161,7 @@ diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index ed1fd4778f7d2..159ac5b5fc70c 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -54,13 +54,13 @@
    {{.locale.Tr "repo.issues.new.clear_labels"}}
    {{if or .Labels .OrgLabels}} {{range .Labels}} - {{svg "octicon-check"}} {{.Name | RenderEmoji}} + {{svg "octicon-check"}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}}
    {{range .OrgLabels}} - {{svg "octicon-check"}} {{.Name | RenderEmoji}} + {{svg "octicon-check"}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}} {{else}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 10bb6a07f9710..f6e508313b0ed 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -124,12 +124,12 @@
    {{.locale.Tr "repo.issues.new.clear_labels"}}
    {{if or .Labels .OrgLabels}} {{range .Labels}} - {{svg "octicon-check"}} {{.Name | RenderEmoji}} + {{svg "octicon-check"}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}}
    {{range .OrgLabels}} - {{svg "octicon-check"}} {{.Name | RenderEmoji}} + {{svg "octicon-check"}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}} {{else}} diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 1c09aade3520b..8a4fad9ac5339 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -238,7 +238,7 @@ {{if or .Labels .Assignees}}
    {{range .Labels}} - {{.Name | RenderEmoji}} + {{RenderLabel .}} {{end}}
    {{range .Assignees}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 93d54930ffdd3..3ceffe754e4d1 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -42,7 +42,7 @@ {{end}} {{range .Labels}} - {{.Name | RenderEmoji}} + {{RenderLabel .}} {{end}}
    diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 07c73ff5cf152..6ae4c73d7952b 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -30,20 +30,14 @@ import {SvgIcon} from '../svg.js'; const {appSubUrl, i18n} = window.config; // NOTE: see models/issue_label.go for similar implementation -const srgbToLinear = (color) => { - color /= 255; - if (color <= 0.04045) { - return color / 12.92; - } - return ((color + 0.055) / 1.055) ** 2.4; -}; -const luminance = (colorString) => { - const r = srgbToLinear(parseInt(colorString.substring(0, 2), 16)); - const g = srgbToLinear(parseInt(colorString.substring(2, 4), 16)); - const b = srgbToLinear(parseInt(colorString.substring(4, 6), 16)); - return 0.2126 * r + 0.7152 * g + 0.0722 * b; +const useLightTextColor = (label) => { + // sRGB color space luminance + const r = parseInt(label.color.substring(0, 2), 16); + const g = parseInt(label.color.substring(2, 4), 16); + const b = parseInt(label.color.substring(4, 6), 16); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance < 0.35; }; -const luminanceThreshold = 0.179; export default { components: {SvgIcon}, @@ -92,10 +86,10 @@ export default { labels() { return this.issue.labels.map((label) => { let textColor; - if (luminance(label.color) < luminanceThreshold) { - textColor = '#ffffff'; + if (useLightTextColor(label)) { + textColor = '#eeeeee'; } else { - textColor = '#000000'; + textColor = '#111111'; } return {name: label.name, color: `#${label.color}`, textColor}; }); diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index f6d6c89816c7a..d89a9f05c6868 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -183,26 +183,19 @@ export function initRepoProject() { } function setLabelColor(label, color) { - const red = getRelativeColor(parseInt(color.slice(1, 3), 16)); - const green = getRelativeColor(parseInt(color.slice(3, 5), 16)); - const blue = getRelativeColor(parseInt(color.slice(5, 7), 16)); - const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + // sRGB color space luminance + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - if (luminance > 0.179) { - label.removeClass('light-label').addClass('dark-label'); - } else { + if (luminance < 0.35) { label.removeClass('dark-label').addClass('light-label'); + } else { + label.removeClass('light-label').addClass('dark-label'); } } -/** - * Inspired by W3C recommendation https://www.w3.org/TR/WCAG20/#relativeluminancedef - */ -function getRelativeColor(color) { - color /= 255; - return color <= 0.03928 ? color / 12.92 : ((color + 0.055) / 1.055) ** 2.4; -} - function rgbToHex(rgb) { rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/); return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`; diff --git a/web_src/less/_base.less b/web_src/less/_base.less index e58bf53f5df9f..b5317250b1732 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -2536,8 +2536,7 @@ table th[data-sortt-desc] { border-top: none; a { - font-size: 15px; - padding-top: 5px; + font-size: 12px; padding-right: 10px; color: var(--color-text-light); @@ -2549,10 +2548,6 @@ table th[data-sortt-desc] { margin-right: 30px; } } - - .ui.label { - font-size: 1em; - } } .item:last-child { diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 5d30d0d81ae7b..396db462b9363 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -92,7 +92,7 @@ .metas { .menu { overflow-x: auto; - max-height: 300px; + max-height: 500px; } .ui.list { @@ -155,12 +155,6 @@ } .filter.menu { - .label.color { - border-radius: 3px; - margin-left: 15px; - padding: 0 8px; - } - &.labels { .label-filter .menu .info { display: inline-block; @@ -181,7 +175,7 @@ } .menu { - max-height: 300px; + max-height: 500px; overflow-x: auto; right: 0 !important; left: auto !important; @@ -190,7 +184,7 @@ .select-label { .desc { - padding-left: 16px; + padding-left: 23px; } } @@ -607,7 +601,7 @@ min-width: 220px; .filter.menu { - max-height: 300px; + max-height: 500px; overflow-x: auto; } } From 2fb26a4bd995bd39484a959a81d561d738c3e85c Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sun, 5 Feb 2023 05:29:29 +0100 Subject: [PATCH 02/10] Label creation and editing in multiline modal menu * Change label creation to open a modal menu like label editing. * Change menu layout to place name, description and colors on separate lines. * Don't color cancel button red in label editing modal menu. * Align text to the left in model menu for better readability and consistent with settings layout elsewhere. --- options/locale/locale_en-US.ini | 6 +-- .../repo/issue/labels/edit_delete_label.tmpl | 31 +++++++------ templates/repo/issue/labels/label_new.tmpl | 44 ++++++++++++------- web_src/js/features/comp/LabelEdit.js | 16 ++++--- web_src/less/_base.less | 1 + web_src/less/_repository.less | 11 ++--- 6 files changed, 62 insertions(+), 47 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index bc2e8cb91cfba..4ecb5d2e1599e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1391,9 +1391,9 @@ issues.sign_in_require_desc = Sign in to join this conversation issues.edit = Edit issues.cancel = Cancel issues.save = Save -issues.label_title = Label name -issues.label_description = Label description -issues.label_color = Label color +issues.label_title = Name +issues.label_description = Description +issues.label_color = Color issues.label_count = %d labels issues.label_open_issues = %d open issues/pull requests issues.label_edit = Edit diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl index a0479dde1bf3f..a9bcac416f1d7 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/edit_delete_label.tmpl @@ -26,31 +26,34 @@
    {{.CsrfTokenHtml}} -
    -
    -
    - -
    +
    + +
    +
    -
    -
    - -
    +
    +
    + +
    +
    +
    +
    +
    -
    -
    - {{template "repo/issue/label_precolors"}} +
    + {{template "repo/issue/label_precolors"}} +
    -
    +
    {{.locale.Tr "cancel"}}
    -
    +
    {{.locale.Tr "save"}}
    diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index 035a4db8000fc..13de043f53c79 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -1,27 +1,39 @@ -
    -
    - {{.CsrfTokenHtml}} -
    -
    + +
    +
    + + +
    +
    + {{.locale.Tr "repo.issues.label_exclusive_desc" | Safe}} +
    diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 7b9935ebbab71..f49ed8770ead0 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -44,10 +44,10 @@
    diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index 13de043f53c79..939b0324a2579 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -11,6 +11,14 @@
    +
    +
    + + +
    +
    + {{.locale.Tr "repo.issues.label_exclusive_desc" | Safe}} +
    diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 822157a78ef97..904e0bb9a0920 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -51,7 +51,8 @@ {{.locale.Tr "repo.issues.filter_label_exclude" | Safe}} {{.locale.Tr "repo.issues.filter_label_no_select"}} {{range .Labels}} - {{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if .IsSelected}}{{svg "octicon-check"}}{{end}} {{RenderLabel .}} + {{$exclusiveScope := .ExclusiveScope}} + {{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if .IsSelected}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel .}} {{end}}
    @@ -218,8 +219,9 @@ diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 159ac5b5fc70c..b56431e6216bf 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -54,13 +54,15 @@
    {{.locale.Tr "repo.issues.new.clear_labels"}}
    {{if or .Labels .OrgLabels}} {{range .Labels}} - {{svg "octicon-check"}}  {{RenderLabel .}} + {{$exclusiveScope := .ExclusiveScope}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}}
    {{range .OrgLabels}} - {{svg "octicon-check"}}  {{RenderLabel .}} + {{$exclusiveScope := .ExclusiveScope}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}} {{else}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index f6e508313b0ed..6bff0aeebc932 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -124,12 +124,14 @@
    {{.locale.Tr "repo.issues.new.clear_labels"}}
    {{if or .Labels .OrgLabels}} {{range .Labels}} - {{svg "octicon-check"}}  {{RenderLabel .}} + {{$exclusiveScope := .ExclusiveScope}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}}
    {{range .OrgLabels}} - {{svg "octicon-check"}}  {{RenderLabel .}} + {{$exclusiveScope := .ExclusiveScope}} + {{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}  {{RenderLabel .}} {{if .Description}}
    {{.Description | RenderEmoji}}{{end}}
    {{end}} {{else}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d6e3009b35b0d..d45a67dc0234d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15347,6 +15347,11 @@ "type": "string", "x-go-name": "Description" }, + "exclusive": { + "type": "boolean", + "x-go-name": "Exclusive", + "example": false + }, "name": { "type": "string", "x-go-name": "Name" @@ -16276,12 +16281,18 @@ "properties": { "color": { "type": "string", - "x-go-name": "Color" + "x-go-name": "Color", + "example": "#00aabb" }, "description": { "type": "string", "x-go-name": "Description" }, + "exclusive": { + "type": "boolean", + "x-go-name": "Exclusive", + "example": false + }, "name": { "type": "string", "x-go-name": "Name" @@ -17603,6 +17614,11 @@ "type": "string", "x-go-name": "Description" }, + "exclusive": { + "type": "boolean", + "x-go-name": "Exclusive", + "example": false + }, "id": { "type": "integer", "format": "int64", @@ -20984,4 +21000,4 @@ "TOTPHeader": [] } ] -} +} \ No newline at end of file diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 2f27978a371d6..4344c15ea45f3 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -174,7 +174,7 @@ func TestAPISearchIssues(t *testing.T) { token := getUserToken(t, "user2") // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 15 // from the fixtures + expectedIssueCount := 16 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } @@ -198,7 +198,7 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 8) + assert.Len(t, apiIssues, 9) query.Del("since") query.Del("before") @@ -214,15 +214,15 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count")) - assert.Len(t, apiIssues, 17) + assert.EqualValues(t, "18", resp.Header().Get("X-Total-Count")) + assert.Len(t, apiIssues, 18) query.Add("limit", "10") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "18", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 10) query = url.Values{"assigned": {"true"}, "state": {"all"}, "token": {token}} @@ -251,7 +251,7 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 6) + assert.Len(t, apiIssues, 7) query = url.Values{"owner": {"user3"}, "token": {token}} // organization link.RawQuery = query.Encode() @@ -272,7 +272,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 15 // from the fixtures + expectedIssueCount := 16 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go index 6e80ebc19c2f2..29fff8ba72618 100644 --- a/tests/integration/api_nodeinfo_test.go +++ b/tests/integration/api_nodeinfo_test.go @@ -34,7 +34,7 @@ func TestNodeinfo(t *testing.T) { assert.True(t, nodeinfo.OpenRegistrations) assert.Equal(t, "gitea", nodeinfo.Software.Name) assert.Equal(t, 24, nodeinfo.Usage.Users.Total) - assert.Equal(t, 17, nodeinfo.Usage.LocalPosts) + assert.Equal(t, 18, nodeinfo.Usage.LocalPosts) assert.Equal(t, 2, nodeinfo.Usage.LocalComments) }) } diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index c913a2000c892..eccf3c3795d9d 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -356,7 +356,7 @@ func TestSearchIssues(t *testing.T) { session := loginUser(t, "user2") - expectedIssueCount := 15 // from the fixtures + expectedIssueCount := 16 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } @@ -377,7 +377,7 @@ func TestSearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 8) + assert.Len(t, apiIssues, 9) query.Del("since") query.Del("before") @@ -393,15 +393,15 @@ func TestSearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count")) - assert.Len(t, apiIssues, 17) + assert.EqualValues(t, "18", resp.Header().Get("X-Total-Count")) + assert.Len(t, apiIssues, 18) query.Add("limit", "5") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "18", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 5) query = url.Values{"assigned": {"true"}, "state": {"all"}} @@ -430,7 +430,7 @@ func TestSearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 6) + assert.Len(t, apiIssues, 7) query = url.Values{"owner": {"user3"}} // organization link.RawQuery = query.Encode() @@ -450,7 +450,7 @@ func TestSearchIssues(t *testing.T) { func TestSearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() - expectedIssueCount := 15 // from the fixtures + expectedIssueCount := 16 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } diff --git a/web_src/js/features/common-issue.js b/web_src/js/features/common-issue.js index 4a62089c60ca5..f53dd5081bf24 100644 --- a/web_src/js/features/common-issue.js +++ b/web_src/js/features/common-issue.js @@ -32,7 +32,7 @@ export function initCommonIssue() { syncIssueSelectionState(); }); - $('.issue-action').on('click', async function () { + $('.issue-action').on('click', async function (e) { let action = this.getAttribute('data-action'); let elementId = this.getAttribute('data-element-id'); const url = this.getAttribute('data-url'); @@ -43,6 +43,9 @@ export function initCommonIssue() { elementId = ''; action = 'clear'; } + if (action === 'toggle' && e.altKey) { + action = 'toggle-alt'; + } updateIssuesMeta( url, action, diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js index a7307fddc397e..b7dccef6ab2b2 100644 --- a/web_src/js/features/comp/LabelEdit.js +++ b/web_src/js/features/comp/LabelEdit.js @@ -20,6 +20,7 @@ export function initCompLabelEdit(selector) { $('.edit-label .color-picker').minicolors('value', $(this).data('color')); $('#label-modal-id').val($(this).data('id')); $('.edit-label .new-label-input').val($(this).data('title')); + $('.edit-label .new-label-exclusive').prop('checked', $(this).data('exclusive')); $('.edit-label .new-label-desc-input').val($(this).data('description')); $('.edit-label .color-picker').val($(this).data('color')); $('.edit-label .minicolors-swatch-color').css('background-color', $(this).data('color')); diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 37366578e2d27..5e6189aed0b6b 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -110,35 +110,59 @@ export function initRepoCommentForm() { } hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var - if ($(this).hasClass('checked')) { - $(this).removeClass('checked'); - $(this).find('.octicon-check').addClass('invisible'); - if (hasUpdateAction) { - if (!($(this).data('id') in items)) { - items[$(this).data('id')] = { - 'update-url': $listMenu.data('update-url'), - action: 'detach', - 'issue-id': $listMenu.data('issue-id'), - }; - } else { - delete items[$(this).data('id')]; + + const clickedItem = $(this); + const scope = $(this).attr('data-scope'); + const canRemoveScope = e.altKey; + + $(this).parent().find('.item').each(function () { + if (scope !== '') { + /* Enable only clicked item for scoped labels. */ + if ($(this).attr('data-scope') !== scope) { + return true; } + if ($(this).is(clickedItem)) { + if (!canRemoveScope && $(this).hasClass('checked')) { + return true; + } + } else if (!$(this).hasClass('checked')) { + return true; + } + } else if (!$(this).is(clickedItem)) { + /* Toggle for other labels. */ + return true; } - } else { - $(this).addClass('checked'); - $(this).find('.octicon-check').removeClass('invisible'); - if (hasUpdateAction) { - if (!($(this).data('id') in items)) { - items[$(this).data('id')] = { - 'update-url': $listMenu.data('update-url'), - action: 'attach', - 'issue-id': $listMenu.data('issue-id'), - }; - } else { - delete items[$(this).data('id')]; + + if ($(this).hasClass('checked')) { + $(this).removeClass('checked'); + $(this).find('.octicon-check').addClass('invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'detach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } + } + } else { + $(this).addClass('checked'); + $(this).find('.octicon-check').removeClass('invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'attach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } } } - } + }); // TODO: Which thing should be done for choosing review requests // to make chosen items be shown on time here? From 3513f74c666e9d780cb486bf6de663ab703f7ae8 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sun, 5 Feb 2023 05:25:04 +0100 Subject: [PATCH 04/10] Exclusive scoped labels custom rendering * Display scoped label prefix and suffix with slightly darker and lighter background color respectively, and a slanted edge between them similar to the `/` symbol. * In menus exclusive labels are grouped with a divider line. --- modules/templates/helper.go | 54 ++++++++++++++++++- templates/repo/issue/list.tmpl | 10 ++++ templates/repo/issue/new_form.tmpl | 10 ++++ .../repo/issue/view_content/sidebar.tmpl | 10 ++++ templates/swagger/v1_json.tmpl | 2 +- web_src/less/_repository.less | 29 ++++++++++ 6 files changed, 112 insertions(+), 3 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b6e626a788e8c..e6e71f0c77cc5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -7,10 +7,12 @@ package templates import ( "bytes" "context" + "encoding/hex" "errors" "fmt" "html" "html/template" + "math" "mime" "net/url" "path/filepath" @@ -803,6 +805,8 @@ func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[str // RenderLabel renders a label func RenderLabel(label *issues_model.Label) string { + labelScope := label.ExclusiveScope() + textColor := "#111" if label.UseLightTextColor() { textColor = "#eee" @@ -810,8 +814,54 @@ func RenderLabel(label *issues_model.Label) string { description := emoji.ReplaceAliases(label.Description) - return fmt.Sprintf("
    %s
    ", - textColor, label.Color, description, RenderEmoji(label.Name)) + if labelScope == "" { + // Regular label + return fmt.Sprintf("
    %s
    ", + textColor, label.Color, description, RenderEmoji(label.Name)) + } + + // Scoped label + scopeText := RenderEmoji(labelScope) + itemText := RenderEmoji(label.Name[len(labelScope)+1:]) + + itemColor := label.Color + scopeColor := label.Color + if r, g, b, err := label.ColorRGB(); err == nil { + // Make scope and item background colors slightly darker and lighter respectively. + // More contrast needed with higher luminance, empirically tweaked. + luminance := (0.299*r + 0.587*g + 0.114*b) / 255 + contrast := 0.01 + luminance*0.06 + // Ensure we add the same amount of contrast also near 0 and 1. + darken := contrast + math.Max(luminance+contrast-1.0, 0.0) + lighten := contrast + math.Max(contrast-luminance, 0.0) + // Compute factor to keep RGB values proportional. + 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) + + 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)), + } + 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)), + } + + itemColor = "#" + hex.EncodeToString(itemBytes) + scopeColor = "#" + hex.EncodeToString(scopeBytes) + } + + return fmt.Sprintf(""+ + "
    %s
    "+ + "
     
    "+ + "
    %s
    "+ + "
    ", + description, + textColor, scopeColor, scopeText, + itemColor, scopeColor, + textColor, itemColor, itemText) } // RenderEmoji renders html text with emoji post processors diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 904e0bb9a0920..11c2385b85d23 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -50,8 +50,13 @@
    {{.locale.Tr "repo.issues.filter_label_exclude" | Safe}} {{.locale.Tr "repo.issues.filter_label_no_select"}} + {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} + {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} +
    + {{end}} + {{$previousExclusiveScope = $exclusiveScope}} {{if .IsExcluded}}{{svg "octicon-circle-slash"}}{{else if .IsSelected}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel .}} {{end}}
    @@ -218,8 +223,13 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 6ae4c73d7952b..3244034782d67 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -26,19 +26,10 @@