mirror of https://github.com/go-gitea/gitea
Merge branch 'main' into actions_support_workflow_dispatch_event
This commit is contained in:
commit
eb714523c1
|
@ -114,7 +114,7 @@ func showWebStartupMessage(msg string) {
|
|||
log.Info("* WorkPath: %s", setting.AppWorkPath)
|
||||
log.Info("* CustomPath: %s", setting.CustomPath)
|
||||
log.Info("* ConfigFile: %s", setting.CustomConf)
|
||||
log.Info("%s", msg)
|
||||
log.Info("%s", msg) // show startup message
|
||||
}
|
||||
|
||||
func serveInstall(ctx *cli.Context) error {
|
||||
|
|
|
@ -76,23 +76,14 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
|
|||
// CalcFingerprint calculate public key's fingerprint
|
||||
func CalcFingerprint(publicKeyContent string) (string, error) {
|
||||
// Call the method based on configuration
|
||||
var (
|
||||
fnName, fp string
|
||||
err error
|
||||
)
|
||||
if len(setting.SSH.KeygenPath) == 0 {
|
||||
fnName = "calcFingerprintNative"
|
||||
fp, err = calcFingerprintNative(publicKeyContent)
|
||||
} else {
|
||||
fnName = "calcFingerprintSSHKeygen"
|
||||
fp, err = calcFingerprintSSHKeygen(publicKeyContent)
|
||||
}
|
||||
useNative := setting.SSH.KeygenPath == ""
|
||||
calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
|
||||
fp, err := calcFn(publicKeyContent)
|
||||
if err != nil {
|
||||
if IsErrKeyUnableVerify(err) {
|
||||
log.Info("%s", publicKeyContent)
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("%s: %w", fnName, err)
|
||||
return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool {
|
|||
var u *RepoUnit
|
||||
var err error
|
||||
if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
|
||||
log.Trace("%s", err)
|
||||
log.Trace("IsDependenciesEnabled: %v", err)
|
||||
return setting.Service.DefaultEnableDependencies
|
||||
}
|
||||
return u.IssuesConfig().EnableDependencies
|
||||
|
|
|
@ -58,6 +58,7 @@ type Package struct {
|
|||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
ReleaseNotes string `json:"release_notes,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Authors string `json:"authors,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
|
@ -71,6 +72,7 @@ type Dependency struct {
|
|||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/nuget/reference/nuspec
|
||||
type nuspecPackage struct {
|
||||
Metadata struct {
|
||||
ID string `xml:"id"`
|
||||
|
@ -80,6 +82,7 @@ type nuspecPackage struct {
|
|||
ProjectURL string `xml:"projectUrl"`
|
||||
Description string `xml:"description"`
|
||||
ReleaseNotes string `xml:"releaseNotes"`
|
||||
Readme string `xml:"readme"`
|
||||
PackageTypes struct {
|
||||
PackageType []struct {
|
||||
Name string `xml:"name,attr"`
|
||||
|
@ -89,6 +92,11 @@ type nuspecPackage struct {
|
|||
URL string `xml:"url,attr"`
|
||||
} `xml:"repository"`
|
||||
Dependencies struct {
|
||||
Dependency []struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
Exclude string `xml:"exclude,attr"`
|
||||
} `xml:"dependency"`
|
||||
Group []struct {
|
||||
TargetFramework string `xml:"targetFramework,attr"`
|
||||
Dependency []struct {
|
||||
|
@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
return ParseNuspecMetaData(f)
|
||||
return ParseNuspecMetaData(archive, f)
|
||||
}
|
||||
}
|
||||
return nil, ErrMissingNuspecFile
|
||||
}
|
||||
|
||||
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
|
||||
func ParseNuspecMetaData(r io.Reader) (*Package, error) {
|
||||
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||
var p nuspecPackage
|
||||
if err := xml.NewDecoder(r).Decode(&p); err != nil {
|
||||
return nil, err
|
||||
|
@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
|
|||
Dependencies: make(map[string][]Dependency),
|
||||
}
|
||||
|
||||
if p.Metadata.Readme != "" {
|
||||
f, err := archive.Open(p.Metadata.Readme)
|
||||
if err == nil {
|
||||
buf, _ := io.ReadAll(f)
|
||||
m.Readme = string(buf)
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.Metadata.Dependencies.Dependency) > 0 {
|
||||
deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
|
||||
for _, dep := range p.Metadata.Dependencies.Dependency {
|
||||
if dep.ID == "" || dep.Version == "" {
|
||||
continue
|
||||
}
|
||||
deps = append(deps, Dependency{
|
||||
ID: dep.ID,
|
||||
Version: dep.Version,
|
||||
})
|
||||
}
|
||||
m.Dependencies[""] = deps
|
||||
}
|
||||
for _, group := range p.Metadata.Dependencies.Group {
|
||||
deps := make([]Dependency, 0, len(group.Dependency))
|
||||
for _, dep := range group.Dependency {
|
||||
|
|
|
@ -6,7 +6,6 @@ package nuget
|
|||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -19,6 +18,7 @@ const (
|
|||
projectURL = "https://gitea.io"
|
||||
description = "Package Description"
|
||||
releaseNotes = "Package Release Notes"
|
||||
readme = "Readme"
|
||||
repositoryURL = "https://gitea.io/gitea/gitea"
|
||||
targetFramework = ".NETStandard2.1"
|
||||
dependencyID = "System.Text.Json"
|
||||
|
@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
|
|||
<description>` + description + `</description>
|
||||
<releaseNotes>` + releaseNotes + `</releaseNotes>
|
||||
<repository url="` + repositoryURL + `" />
|
||||
<readme>README.md</readme>
|
||||
<dependencies>
|
||||
<group targetFramework="` + targetFramework + `">
|
||||
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
|
||||
|
@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
|
|||
</package>`
|
||||
|
||||
func TestParsePackageMetaData(t *testing.T) {
|
||||
createArchive := func(name, content string) []byte {
|
||||
createArchive := func(files map[string]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
archive := zip.NewWriter(&buf)
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
for name, content := range files {
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
archive.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
t.Run("MissingNuspecFile", func(t *testing.T) {
|
||||
data := createArchive("dummy.txt", "")
|
||||
data := createArchive(map[string]string{"dummy.txt": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
|
||||
data := createArchive("sub/package.nuspec", "")
|
||||
data := createArchive(map[string]string{"sub/package.nuspec": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidNuspecFile", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", "")
|
||||
data := createArchive(map[string]string{"package.nuspec": ""})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidPackageId", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata></metadata>
|
||||
</package>`)
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
|
@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>`+id+`</id>
|
||||
<id>` + id + `</id>
|
||||
</metadata>
|
||||
</package>`)
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, np)
|
||||
assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
data := createArchive("package.nuspec", nuspecContent)
|
||||
t.Run("MissingReadme", func(t *testing.T) {
|
||||
data := createArchive(map[string]string{"package.nuspec": nuspecContent})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Empty(t, np.Metadata.Readme)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseNuspecMetaData(t *testing.T) {
|
||||
t.Run("Dependency Package", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
|
||||
data := createArchive(map[string]string{
|
||||
"package.nuspec": nuspecContent,
|
||||
"README.md": readme,
|
||||
})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, DependencyPackage, np.PackageType)
|
||||
|
@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
assert.Equal(t, projectURL, np.Metadata.ProjectURL)
|
||||
assert.Equal(t, description, np.Metadata.Description)
|
||||
assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
|
||||
assert.Equal(t, readme, np.Metadata.Readme)
|
||||
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
|
||||
assert.Len(t, np.Metadata.Dependencies, 1)
|
||||
assert.Contains(t, np.Metadata.Dependencies, targetFramework)
|
||||
|
@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
assert.Equal(t, dependencyVersion, deps[0].Version)
|
||||
|
||||
t.Run("NormalizedVersion", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>test</id>
|
||||
<version>1.04.5.2.5-rc.1+metadata</version>
|
||||
</metadata>
|
||||
</package>`))
|
||||
data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>test</id>
|
||||
<version>1.04.5.2.5-rc.1+metadata</version>
|
||||
</metadata>
|
||||
</package>`})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, "1.4.5.2-rc.1", np.Version)
|
||||
|
@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Symbols Package", func(t *testing.T) {
|
||||
np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
|
||||
data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
|
||||
|
||||
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, np)
|
||||
assert.Equal(t, SymbolsPackage, np.PackageType)
|
||||
|
|
|
@ -53,13 +53,13 @@ func NewFuncMap() template.FuncMap {
|
|||
"JsonUtils": NewJsonUtils,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// svg / avatar / icon
|
||||
// svg / avatar / icon / color
|
||||
"svg": svg.RenderHTML,
|
||||
"EntryIcon": base.EntryIcon,
|
||||
"MigrationIcon": MigrationIcon,
|
||||
"ActionIcon": ActionIcon,
|
||||
|
||||
"SortArrow": SortArrow,
|
||||
"SortArrow": SortArrow,
|
||||
"ContrastColor": util.ContrastColor,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
|
|
|
@ -123,16 +123,10 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
|
|||
func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
|
||||
var (
|
||||
archivedCSSClass string
|
||||
textColor = "#111"
|
||||
textColor = util.ContrastColor(label.Color)
|
||||
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
|
||||
if util.UseLightTextOnBackground(r, g, b) {
|
||||
textColor = "#eee"
|
||||
}
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
|
||||
if label.IsArchived() {
|
||||
|
@ -153,7 +147,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
|
||||
// Make scope and item background colors slightly darker and lighter respectively.
|
||||
// More contrast needed with higher luminance, empirically tweaked.
|
||||
luminance := util.GetLuminance(r, g, b)
|
||||
luminance := util.GetRelativeLuminance(label.Color)
|
||||
contrast := 0.01 + luminance*0.03
|
||||
// Ensure we add the same amount of contrast also near 0 and 1.
|
||||
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
||||
|
@ -162,6 +156,7 @@ func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_m
|
|||
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)
|
||||
|
||||
r, g, b := util.HexToRBGColor(label.Color)
|
||||
scopeBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
||||
|
|
|
@ -4,22 +4,10 @@ package util
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Check similar implementation in web_src/js/utils/color.js and keep synchronization
|
||||
|
||||
// Return R, G, B values defined in reletive luminance
|
||||
func getLuminanceRGB(channel float64) float64 {
|
||||
sRGB := channel / 255
|
||||
if sRGB <= 0.03928 {
|
||||
return sRGB / 12.92
|
||||
}
|
||||
return math.Pow((sRGB+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
// Get color as RGB values in 0..255 range from the hex color string (with or without #)
|
||||
func HexToRBGColor(colorString string) (float64, float64, float64) {
|
||||
hexString := colorString
|
||||
|
@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
|
|||
return r, g, b
|
||||
}
|
||||
|
||||
// return luminance given RGB channels
|
||||
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
|
||||
func GetLuminance(r, g, b float64) float64 {
|
||||
R := getLuminanceRGB(r)
|
||||
G := getLuminanceRGB(g)
|
||||
B := getLuminanceRGB(b)
|
||||
luminance := 0.2126*R + 0.7152*G + 0.0722*B
|
||||
return luminance
|
||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||
// Keep this in sync with web_src/js/utils/color.js
|
||||
func GetRelativeLuminance(color string) float64 {
|
||||
r, g, b := HexToRBGColor(color)
|
||||
return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
|
||||
}
|
||||
|
||||
// Reference from: https://firsching.ch/github_labels.html
|
||||
// In the future WCAG 3 APCA may be a better solution.
|
||||
// Check if text should use light color based on RGB of background
|
||||
func UseLightTextOnBackground(r, g, b float64) bool {
|
||||
return GetLuminance(r, g, b) < 0.453
|
||||
func UseLightText(backgroundColor string) bool {
|
||||
return GetRelativeLuminance(backgroundColor) < 0.453
|
||||
}
|
||||
|
||||
// Given a background color, returns a black or white foreground color that the highest
|
||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||
func ContrastColor(backgroundColor string) string {
|
||||
if UseLightText(backgroundColor) {
|
||||
return "#fff"
|
||||
}
|
||||
return "#000"
|
||||
}
|
||||
|
|
|
@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_UseLightTextOnBackground(t *testing.T) {
|
||||
func Test_UseLightText(t *testing.T) {
|
||||
cases := []struct {
|
||||
r float64
|
||||
g float64
|
||||
b float64
|
||||
expected bool
|
||||
color string
|
||||
expected string
|
||||
}{
|
||||
{215, 58, 74, true},
|
||||
{0, 117, 202, true},
|
||||
{207, 211, 215, false},
|
||||
{162, 238, 239, false},
|
||||
{112, 87, 255, true},
|
||||
{0, 134, 114, true},
|
||||
{228, 230, 105, false},
|
||||
{216, 118, 227, true},
|
||||
{255, 255, 255, false},
|
||||
{43, 134, 133, true},
|
||||
{43, 135, 134, true},
|
||||
{44, 135, 134, true},
|
||||
{59, 182, 179, true},
|
||||
{124, 114, 104, true},
|
||||
{126, 113, 108, true},
|
||||
{129, 112, 109, true},
|
||||
{128, 112, 112, true},
|
||||
{"#d73a4a", "#fff"},
|
||||
{"#0075ca", "#fff"},
|
||||
{"#cfd3d7", "#000"},
|
||||
{"#a2eeef", "#000"},
|
||||
{"#7057ff", "#fff"},
|
||||
{"#008672", "#fff"},
|
||||
{"#e4e669", "#000"},
|
||||
{"#d876e3", "#000"},
|
||||
{"#ffffff", "#000"},
|
||||
{"#2b8684", "#fff"},
|
||||
{"#2b8786", "#fff"},
|
||||
{"#2c8786", "#000"},
|
||||
{"#3bb6b3", "#000"},
|
||||
{"#7c7268", "#fff"},
|
||||
{"#7e716c", "#fff"},
|
||||
{"#81706d", "#fff"},
|
||||
{"#807070", "#fff"},
|
||||
{"#84b6eb", "#000"},
|
||||
}
|
||||
for n, c := range cases {
|
||||
result := UseLightTextOnBackground(c.r, c.g, c.b)
|
||||
assert.Equal(t, c.expected, result, "case %d: error should match", n)
|
||||
assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ func ToPointer[T any](val T) *T {
|
|||
}
|
||||
|
||||
// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
|
||||
func Iif[T comparable](condition bool, trueVal, falseVal T) T {
|
||||
func Iif[T any](condition bool, trueVal, falseVal T) T {
|
||||
if condition {
|
||||
return trueVal
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
@ -201,7 +200,6 @@ func ReadRepoNotifications(ctx *context.APIContext) {
|
|||
if !ctx.FormBool("all") {
|
||||
statuses := ctx.FormStrings("status-types")
|
||||
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
|
||||
log.Error("%v", opts.Status)
|
||||
}
|
||||
nl, err := db.Find[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
|
|
|
@ -26,7 +26,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
|
|||
defer rd.Close()
|
||||
|
||||
if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("JSON Decode failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
|
@ -35,7 +35,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
|
|||
|
||||
owner, repo, err := parseScope(ctx, genRequest.Scope)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("parseScope failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
|
@ -45,18 +45,18 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
|
|||
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
|
||||
token, err = actions_model.NewRunnerToken(ctx, owner, repo)
|
||||
if err != nil {
|
||||
err := fmt.Sprintf("error while creating runner token: %v", err)
|
||||
log.Error("%v", err)
|
||||
errMsg := fmt.Sprintf("error while creating runner token: %v", err)
|
||||
log.Error("NewRunnerToken failed: %v", errMsg)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err,
|
||||
Err: errMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
err := fmt.Sprintf("could not get unactivated runner token: %v", err)
|
||||
log.Error("%v", err)
|
||||
errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
|
||||
log.Error("GetLatestRunnerToken failed: %v", errMsg)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err,
|
||||
Err: errMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
|
|||
_ = stdoutWriter.Close()
|
||||
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("readAndVerifyCommitsFromShaReader failed: %v", err)
|
||||
cancel()
|
||||
}
|
||||
_ = stdoutReader.Close()
|
||||
|
@ -66,7 +66,6 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository
|
|||
line := scanner.Text()
|
||||
err := readAndVerifyCommit(line, repo, env)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func SendEmail(ctx *context.PrivateContext) {
|
|||
defer rd.Close()
|
||||
|
||||
if err := json.NewDecoder(rd).Decode(&mail); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("JSON Decode failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
|
|
|
@ -403,7 +403,6 @@ func EditUserPost(ctx *context.Context) {
|
|||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
log.Error("%s", err.Error())
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
|
||||
default:
|
||||
|
|
|
@ -214,7 +214,6 @@ func ResetPasswdPost(ctx *context.Context) {
|
|||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
log.Error("%s", err.Error())
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
|
||||
default:
|
||||
ctx.ServerError("UpdateAuth", err)
|
||||
|
@ -298,7 +297,6 @@ func MustChangePasswordPost(ctx *context.Context) {
|
|||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
log.Error("%s", err.Error())
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
|
||||
default:
|
||||
|
|
|
@ -74,7 +74,6 @@ func AccountPost(ctx *context.Context) {
|
|||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
log.Error("%s", err.Error())
|
||||
ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
|
||||
default:
|
||||
ctx.ServerError("UpdateAuth", err)
|
||||
|
|
|
@ -79,11 +79,11 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) {
|
|||
case setting.CfTurnstile:
|
||||
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
|
||||
default:
|
||||
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
|
||||
ctx.ServerError("Unknown Captcha Type", fmt.Errorf("unknown Captcha Type: %s", setting.Service.CaptchaType))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Debug("%v", err)
|
||||
log.Debug("Captcha Verify failed: %v", err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
|
|
|
@ -91,7 +91,7 @@ func AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues
|
|||
// NewPullRequest notifies new pull request to notifiers
|
||||
func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
|
||||
if err := pr.LoadIssue(ctx); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("LoadIssue failed: %v", err)
|
||||
return
|
||||
}
|
||||
if err := pr.Issue.LoadPoster(ctx); err != nil {
|
||||
|
@ -112,7 +112,7 @@ func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *iss
|
|||
// PullRequestReview notifies new pull request review
|
||||
func PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
|
||||
if err := review.LoadReviewer(ctx); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("LoadReviewer failed: %v", err)
|
||||
return
|
||||
}
|
||||
for _, notifier := range notifiers {
|
||||
|
|
|
@ -28,7 +28,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
|
|||
|
||||
t, err := NewTemporaryUploadRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("NewTemporaryUploadRepository failed: %v", err)
|
||||
}
|
||||
defer t.Close()
|
||||
if err := t.Clone(opts.OldBranch, false); err != nil {
|
||||
|
|
|
@ -111,7 +111,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
|
|||
|
||||
t, err := NewTemporaryUploadRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("NewTemporaryUploadRepository failed: %v", err)
|
||||
}
|
||||
defer t.Close()
|
||||
if err := t.Clone(opts.OldBranch, true); err != nil {
|
||||
|
|
|
@ -143,7 +143,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
|||
|
||||
t, err := NewTemporaryUploadRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("NewTemporaryUploadRepository failed: %v", err)
|
||||
}
|
||||
defer t.Close()
|
||||
hasOldBranch := true
|
||||
|
|
|
@ -161,7 +161,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||
if isOldWikiExist {
|
||||
err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("RemoveFilesFromIndex failed: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -171,18 +171,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||
|
||||
objectHash, err := gitRepo.HashObject(strings.NewReader(content))
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("HashObject failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("AddObjectToIndex failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
tree, err := gitRepo.WriteTree()
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("WriteTree failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||
|
||||
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("CommitTree failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -222,11 +222,11 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||
0,
|
||||
),
|
||||
}); err != nil {
|
||||
log.Error("%v", err)
|
||||
log.Error("Push failed: %v", err)
|
||||
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Push: %w", err)
|
||||
return fmt.Errorf("failed to push: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}}
|
||||
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
|
||||
{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{.PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
|
||||
</div>
|
||||
{{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .PackageDescriptor.Metadata.Dependencies}}
|
||||
|
|
|
@ -66,13 +66,13 @@
|
|||
<div id="project-board">
|
||||
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
|
||||
{{range .Columns}}
|
||||
<div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||
<div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||
<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
|
||||
<div class="ui large label project-column-title tw-py-1">
|
||||
<div class="ui small circular grey label project-column-issue-count">
|
||||
{{.NumIssues ctx}}
|
||||
</div>
|
||||
{{.Title}}
|
||||
<span class="project-column-title-label">{{.Title}}</span>
|
||||
</div>
|
||||
{{if $canWriteProject}}
|
||||
<div class="ui dropdown jump item">
|
||||
|
@ -153,9 +153,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div>
|
||||
<div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}">
|
||||
{{range (index $.IssuesMap .ID)}}
|
||||
<div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
|
||||
<div class="ui container">
|
||||
{{$notificationUnreadCount := call .NotificationUnreadCount}}
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]">
|
||||
<div class="small-menu-items ui compact tiny menu">
|
||||
<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">
|
||||
{{ctx.Locale.Tr "notification.unread"}}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
|
||||
.run-list-item-right {
|
||||
flex: 0 0 15%;
|
||||
flex: 0 0 min(20%, 130px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
|
||||
--tab-size: 4;
|
||||
--checkbox-size: 16px; /* height and width of checkbox and radio inputs */
|
||||
--page-spacing: 16px; /* space between page elements */
|
||||
}
|
||||
|
||||
:root * {
|
||||
|
@ -582,11 +583,14 @@ img.ui.avatar,
|
|||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* add padding to all content when there is no .secondary.nav. this uses padding instead of
|
||||
margin because with the negative margin on .ui.grid we would have to set margin-top: 0,
|
||||
but that does not work universally for all pages */
|
||||
/* add margin to all pages when there is no .secondary.nav */
|
||||
.page-content > :first-child:not(.secondary-nav) {
|
||||
padding-top: 14px;
|
||||
margin-top: var(--page-spacing);
|
||||
}
|
||||
/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need
|
||||
to compensate here */
|
||||
.page-content > :first-child.ui.grid {
|
||||
margin-top: calc(var(--page-spacing) - 1rem);
|
||||
}
|
||||
|
||||
.ui.pagination.menu .active.item {
|
||||
|
|
|
@ -22,34 +22,27 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.project-column .issue-card {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.project-column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.project-column-header.dark-label {
|
||||
color: var(--color-project-board-dark-label) !important;
|
||||
}
|
||||
|
||||
.project-column-header.dark-label .project-column-title {
|
||||
color: var(--color-project-board-dark-label) !important;
|
||||
}
|
||||
|
||||
.project-column-header.light-label {
|
||||
color: var(--color-project-board-light-label) !important;
|
||||
}
|
||||
|
||||
.project-column-header.light-label .project-column-title {
|
||||
color: var(--color-project-board-light-label) !important;
|
||||
}
|
||||
|
||||
.project-column-title {
|
||||
background: none !important;
|
||||
line-height: 1.25 !important;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.project-column-title,
|
||||
.project-column-issue-count {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.project-column > .cards {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
@ -64,6 +57,8 @@
|
|||
|
||||
.project-column > .divider {
|
||||
margin: 5px 0;
|
||||
border-color: currentcolor;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.project-column:first-child {
|
||||
|
|
|
@ -249,21 +249,6 @@ textarea:focus,
|
|||
.user.signup form .optional .title {
|
||||
margin-left: 250px !important;
|
||||
}
|
||||
.user.activate form .inline.field > input,
|
||||
.user.forgot.password form .inline.field > input,
|
||||
.user.reset.password form .inline.field > input,
|
||||
.user.link-account form .inline.field > input,
|
||||
.user.signin form .inline.field > input,
|
||||
.user.signup form .inline.field > input,
|
||||
.user.activate form .inline.field > textarea,
|
||||
.user.forgot.password form .inline.field > textarea,
|
||||
.user.reset.password form .inline.field > textarea,
|
||||
.user.link-account form .inline.field > textarea,
|
||||
.user.signin form .inline.field > textarea,
|
||||
.user.signup form .inline.field > textarea,
|
||||
.oauth-login-link {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
|
@ -310,14 +295,7 @@ textarea:focus,
|
|||
.user.reset.password form .inline.field > label,
|
||||
.user.link-account form .inline.field > label,
|
||||
.user.signin form .inline.field > label,
|
||||
.user.signup form .inline.field > label,
|
||||
.user.activate form input,
|
||||
.user.forgot.password form input,
|
||||
.user.reset.password form input,
|
||||
.user.link-account form input,
|
||||
.user.signin form input,
|
||||
.user.signup form input,
|
||||
.oauth-login-link {
|
||||
.user.signup form .inline.field > label {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
@ -435,9 +413,9 @@ textarea:focus,
|
|||
.repository.new.repo form label,
|
||||
.repository.new.migrate form label,
|
||||
.repository.new.fork form label,
|
||||
.repository.new.repo form input,
|
||||
.repository.new.migrate form input,
|
||||
.repository.new.fork form input,
|
||||
.repository.new.repo form .inline.field > input,
|
||||
.repository.new.migrate form .inline.field > input,
|
||||
.repository.new.fork form .inline.field > input,
|
||||
.repository.new.fork form .field a,
|
||||
.repository.new.repo form .selection.dropdown,
|
||||
.repository.new.migrate form .selection.dropdown,
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
.flex-container {
|
||||
display: flex !important;
|
||||
gap: 16px;
|
||||
gap: var(--page-spacing);
|
||||
margin-top: var(--page-spacing);
|
||||
}
|
||||
|
||||
.flex-container-nav {
|
||||
|
|
|
@ -48,8 +48,11 @@
|
|||
cursor: default;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 2.67142857em;
|
||||
opacity: 0.5;
|
||||
border-radius: 0 0.28571429rem 0.28571429rem 0;
|
||||
pointer-events: none;
|
||||
|
@ -58,6 +61,8 @@
|
|||
|
||||
.ui.icon.input > i.icon.is-loading {
|
||||
position: absolute !important;
|
||||
height: 28px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.ui.icon.input > i.icon.is-loading > * {
|
||||
|
@ -78,7 +83,7 @@
|
|||
|
||||
.ui[class*="left icon"].input > i.icon {
|
||||
right: auto;
|
||||
left: 8px;
|
||||
left: 1px;
|
||||
border-radius: 0.28571429rem 0 0 0.28571429rem;
|
||||
}
|
||||
.ui[class*="left icon"].input > i.circular.icon {
|
||||
|
|
|
@ -2273,8 +2273,21 @@
|
|||
height: 0.5em;
|
||||
}
|
||||
|
||||
.labels-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.labels-list a {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.labels-list .label {
|
||||
margin: 2px 0;
|
||||
padding: 0 6px;
|
||||
margin: 0 !important;
|
||||
min-height: 20px;
|
||||
display: inline-flex !important;
|
||||
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
|
||||
}
|
||||
|
|
|
@ -34,23 +34,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
#issue-list .flex-item-title .labels-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
#issue-list .flex-item-title .labels-list a {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#issue-list .flex-item-title .labels-list .label {
|
||||
padding: 0 6px;
|
||||
margin: 0;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
#issue-list .flex-item-body .branches {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
--color-console-fg-subtle: #bec4c8;
|
||||
--color-console-bg: #171b1e;
|
||||
--color-console-border: #2e353b;
|
||||
--color-console-hover-bg: #e8e8ff16;
|
||||
--color-console-hover-bg: #292d31;
|
||||
--color-console-active-bg: #2e353b;
|
||||
--color-console-menu-bg: #252b30;
|
||||
--color-console-menu-border: #424b51;
|
||||
|
@ -215,8 +215,6 @@
|
|||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--color-editor-line-highlight: var(--color-primary-light-5);
|
||||
--color-project-board-bg: var(--color-secondary-light-2);
|
||||
--color-project-board-dark-label: #0e1011;
|
||||
--color-project-board-light-label: #dde0e2;
|
||||
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
|
||||
--color-reaction-bg: #e8e8ff12;
|
||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||
|
|
|
@ -63,12 +63,12 @@
|
|||
/* console colors - used for actions console and console files */
|
||||
--color-console-fg: #f8f8f9;
|
||||
--color-console-fg-subtle: #bec4c8;
|
||||
--color-console-bg: #181b1d;
|
||||
--color-console-border: #313538;
|
||||
--color-console-hover-bg: #ffffff16;
|
||||
--color-console-active-bg: #313538;
|
||||
--color-console-menu-bg: #272b2e;
|
||||
--color-console-menu-border: #464a4d;
|
||||
--color-console-bg: #171b1e;
|
||||
--color-console-border: #2e353b;
|
||||
--color-console-hover-bg: #292d31;
|
||||
--color-console-active-bg: #2e353b;
|
||||
--color-console-menu-bg: #252b30;
|
||||
--color-console-menu-border: #424b51;
|
||||
/* named colors */
|
||||
--color-red: #db2828;
|
||||
--color-orange: #f2711c;
|
||||
|
@ -215,8 +215,6 @@
|
|||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--color-editor-line-highlight: var(--color-primary-light-6);
|
||||
--color-project-board-bg: var(--color-secondary-light-4);
|
||||
--color-project-board-dark-label: #0e1114;
|
||||
--color-project-board-light-label: #eaeef2;
|
||||
--color-caret: var(--color-text-dark);
|
||||
--color-reaction-bg: #0000170a;
|
||||
--color-reaction-hover-bg: var(--color-primary-light-5);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {useLightTextOnBackground} from '../utils/color.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {contrastColor} from '../utils/color.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
|
||||
const {appSubUrl, i18n} = window.config;
|
||||
|
@ -59,16 +58,11 @@ export default {
|
|||
},
|
||||
|
||||
labels() {
|
||||
return this.issue.labels.map((label) => {
|
||||
let textColor;
|
||||
const {r, g, b} = tinycolor(label.color).toRgb();
|
||||
if (useLightTextOnBackground(r, g, b)) {
|
||||
textColor = '#eeeeee';
|
||||
} else {
|
||||
textColor = '#111111';
|
||||
}
|
||||
return {name: label.name, color: `#${label.color}`, textColor};
|
||||
});
|
||||
return this.issue.labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: `#${label.color}`,
|
||||
textColor: contrastColor(`#${label.color}`),
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -108,7 +102,7 @@ export default {
|
|||
<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>{{ body }}</p>
|
||||
<div>
|
||||
<div class="labels-list">
|
||||
<div
|
||||
v-for="label in labels"
|
||||
:key="label.name"
|
||||
|
|
|
@ -517,8 +517,16 @@ export function initRepositoryActionView() {
|
|||
|
||||
.action-commit-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin: 0 0 0 28px;
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.action-commit-summary {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================ */
|
||||
|
@ -531,6 +539,14 @@ export function initRepositoryActionView() {
|
|||
top: 12px;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
background: var(--color-body);
|
||||
z-index: 2; /* above .job-info-header */
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.action-view-left {
|
||||
position: static; /* can not sticky because multiple jobs would overlap into right view */
|
||||
}
|
||||
}
|
||||
|
||||
.job-artifacts-title {
|
||||
|
@ -692,7 +708,9 @@ export function initRepositoryActionView() {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
height: 60px;
|
||||
z-index: 1;
|
||||
z-index: 1; /* above .job-step-container */
|
||||
background: var(--color-console-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.job-info-header:has(+ .job-step-container) {
|
||||
|
@ -730,7 +748,7 @@ export function initRepositoryActionView() {
|
|||
|
||||
.job-step-container .job-step-summary.step-expandable:hover {
|
||||
color: var(--color-console-fg);
|
||||
background-color: var(--color-console-hover-bg);
|
||||
background: var(--color-console-hover-bg);
|
||||
}
|
||||
|
||||
.job-step-container .job-step-summary .step-summary-msg {
|
||||
|
@ -748,17 +766,15 @@ export function initRepositoryActionView() {
|
|||
top: 60px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 767.98px) {
|
||||
.action-view-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
.action-view-left, .action-view-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-view-left {
|
||||
max-width: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import $ from 'jquery';
|
||||
import {useLightTextOnBackground} from '../utils/color.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {contrastColor} from '../utils/color.js';
|
||||
import {createSortable} from '../modules/sortable.js';
|
||||
import {POST, DELETE, PUT} from '../modules/fetch.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
function updateIssueCount(cards) {
|
||||
const parent = cards.parentElement;
|
||||
|
@ -65,14 +65,11 @@ async function initRepoProjectSortable() {
|
|||
boardColumns = mainBoard.getElementsByClassName('project-column');
|
||||
for (let i = 0; i < boardColumns.length; i++) {
|
||||
const column = boardColumns[i];
|
||||
if (parseInt($(column).data('sorting')) !== i) {
|
||||
if (parseInt(column.getAttribute('data-sorting')) !== i) {
|
||||
try {
|
||||
await PUT($(column).data('url'), {
|
||||
data: {
|
||||
sorting: i,
|
||||
color: rgbToHex(window.getComputedStyle($(column)[0]).backgroundColor),
|
||||
},
|
||||
});
|
||||
const bgColor = column.style.backgroundColor; // will be rgb() string
|
||||
const color = bgColor ? tinycolor(bgColor).toHexString() : '';
|
||||
await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -102,16 +99,10 @@ export function initRepoProject() {
|
|||
|
||||
for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
|
||||
const projectHeader = modal.closest('.project-column-header');
|
||||
const projectTitleLabel = projectHeader?.querySelector('.project-column-title');
|
||||
const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
|
||||
const projectTitleInput = modal.querySelector('.project-column-title-input');
|
||||
const projectColorInput = modal.querySelector('#new_project_column_color');
|
||||
const boardColumn = modal.closest('.project-column');
|
||||
const bgColor = boardColumn?.style.backgroundColor;
|
||||
|
||||
if (bgColor) {
|
||||
setLabelColor(projectHeader, rgbToHex(bgColor));
|
||||
}
|
||||
|
||||
modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
|
@ -126,10 +117,21 @@ export function initRepoProject() {
|
|||
} finally {
|
||||
projectTitleLabel.textContent = projectTitleInput?.value;
|
||||
projectTitleInput.closest('form')?.classList.remove('dirty');
|
||||
if (projectColorInput?.value) {
|
||||
setLabelColor(projectHeader, projectColorInput.value);
|
||||
const dividers = boardColumn.querySelectorAll(':scope > .divider');
|
||||
if (projectColorInput.value) {
|
||||
const color = contrastColor(projectColorInput.value);
|
||||
boardColumn.style.setProperty('background', projectColorInput.value, 'important');
|
||||
boardColumn.style.setProperty('color', color, 'important');
|
||||
for (const divider of dividers) {
|
||||
divider.style.setProperty('color', color);
|
||||
}
|
||||
} else {
|
||||
boardColumn.style.removeProperty('background');
|
||||
boardColumn.style.removeProperty('color');
|
||||
for (const divider of dividers) {
|
||||
divider.style.removeProperty('color');
|
||||
}
|
||||
}
|
||||
boardColumn.style = `background: ${projectColorInput.value} !important`;
|
||||
$('.ui.modal').modal('hide');
|
||||
}
|
||||
});
|
||||
|
@ -182,24 +184,3 @@ export function initRepoProject() {
|
|||
createNewColumn(url, $columnTitle, $projectColorInput);
|
||||
});
|
||||
}
|
||||
|
||||
function setLabelColor(label, color) {
|
||||
const {r, g, b} = tinycolor(color).toRgb();
|
||||
if (useLightTextOnBackground(r, g, b)) {
|
||||
label.classList.remove('dark-label');
|
||||
label.classList.add('light-label');
|
||||
} else {
|
||||
label.classList.remove('light-label');
|
||||
label.classList.add('dark-label');
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/);
|
||||
return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`;
|
||||
}
|
||||
|
||||
function hex(x) {
|
||||
const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
// Check similar implementation in modules/util/color.go and keep synchronization
|
||||
// Return R, G, B values defined in reletive luminance
|
||||
function getLuminanceRGB(channel) {
|
||||
const sRGB = channel / 255;
|
||||
return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||
// Keep this in sync with modules/util/color.go
|
||||
function getRelativeLuminance(color) {
|
||||
const {r, g, b} = tinycolor(color).toRgb();
|
||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||
}
|
||||
|
||||
// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
|
||||
function getLuminance(r, g, b) {
|
||||
const R = getLuminanceRGB(r);
|
||||
const G = getLuminanceRGB(g);
|
||||
const B = getLuminanceRGB(b);
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
function useLightText(backgroundColor) {
|
||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||
}
|
||||
|
||||
// Reference from: https://firsching.ch/github_labels.html
|
||||
// In the future WCAG 3 APCA may be a better solution.
|
||||
// Check if text should use light color based on RGB of background
|
||||
export function useLightTextOnBackground(r, g, b) {
|
||||
return getLuminance(r, g, b) < 0.453;
|
||||
// Given a background color, returns a black or white foreground color that the highest
|
||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||
export function contrastColor(backgroundColor) {
|
||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||
}
|
||||
|
||||
function resolveColors(obj) {
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import {useLightTextOnBackground} from './color.js';
|
||||
import {contrastColor} from './color.js';
|
||||
|
||||
test('useLightTextOnBackground', () => {
|
||||
expect(useLightTextOnBackground(215, 58, 74)).toBe(true);
|
||||
expect(useLightTextOnBackground(0, 117, 202)).toBe(true);
|
||||
expect(useLightTextOnBackground(207, 211, 215)).toBe(false);
|
||||
expect(useLightTextOnBackground(162, 238, 239)).toBe(false);
|
||||
expect(useLightTextOnBackground(112, 87, 255)).toBe(true);
|
||||
expect(useLightTextOnBackground(0, 134, 114)).toBe(true);
|
||||
expect(useLightTextOnBackground(228, 230, 105)).toBe(false);
|
||||
expect(useLightTextOnBackground(216, 118, 227)).toBe(true);
|
||||
expect(useLightTextOnBackground(255, 255, 255)).toBe(false);
|
||||
expect(useLightTextOnBackground(43, 134, 133)).toBe(true);
|
||||
expect(useLightTextOnBackground(43, 135, 134)).toBe(true);
|
||||
expect(useLightTextOnBackground(44, 135, 134)).toBe(true);
|
||||
expect(useLightTextOnBackground(59, 182, 179)).toBe(true);
|
||||
expect(useLightTextOnBackground(124, 114, 104)).toBe(true);
|
||||
expect(useLightTextOnBackground(126, 113, 108)).toBe(true);
|
||||
expect(useLightTextOnBackground(129, 112, 109)).toBe(true);
|
||||
expect(useLightTextOnBackground(128, 112, 112)).toBe(true);
|
||||
test('contrastColor', () => {
|
||||
expect(contrastColor('#d73a4a')).toBe('#fff');
|
||||
expect(contrastColor('#0075ca')).toBe('#fff');
|
||||
expect(contrastColor('#cfd3d7')).toBe('#000');
|
||||
expect(contrastColor('#a2eeef')).toBe('#000');
|
||||
expect(contrastColor('#7057ff')).toBe('#fff');
|
||||
expect(contrastColor('#008672')).toBe('#fff');
|
||||
expect(contrastColor('#e4e669')).toBe('#000');
|
||||
expect(contrastColor('#d876e3')).toBe('#000');
|
||||
expect(contrastColor('#ffffff')).toBe('#000');
|
||||
expect(contrastColor('#2b8684')).toBe('#fff');
|
||||
expect(contrastColor('#2b8786')).toBe('#fff');
|
||||
expect(contrastColor('#2c8786')).toBe('#000');
|
||||
expect(contrastColor('#3bb6b3')).toBe('#000');
|
||||
expect(contrastColor('#7c7268')).toBe('#fff');
|
||||
expect(contrastColor('#7e716c')).toBe('#fff');
|
||||
expect(contrastColor('#81706d')).toBe('#fff');
|
||||
expect(contrastColor('#807070')).toBe('#fff');
|
||||
expect(contrastColor('#84b6eb')).toBe('#000');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue