Merge branch 'main' into fix-ui-regression

This commit is contained in:
wxiaoguang 2024-05-06 14:46:36 +08:00 committed by GitHub
commit 7808815e0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 574 additions and 425 deletions

View File

@ -1,7 +1,7 @@
# Gitea - Docker
Dockerfile is found in root of repository.
Dockerfile is found in the root of the repository.
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea)
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea).
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless)
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless).

View File

@ -29,6 +29,7 @@ type GrepOptions struct {
ContextLineNumber int
IsFuzzy bool
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
PathspecList []string
}
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
}
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
cmd.AddDashesAndList(opts.PathspecList...)
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{

View File

@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) {
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "main.vendor.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{

View File

@ -10,6 +10,7 @@ import (
"path"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
@ -54,7 +55,7 @@ var (
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
// anyHashPattern splits url containing SHA into parts
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`)
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
@ -591,7 +592,8 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
func mentionProcessor(ctx *RenderContext, node *html.Node) {
start := 0
for node != nil {
nodeStop := node.NextSibling
for node != nodeStop {
found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:]))
if !found {
node = node.NextSibling
@ -962,57 +964,68 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
}
}
type anyHashPatternResult struct {
PosStart int
PosEnd int
FullURL string
CommitID string
SubPath string
QueryHash string
}
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
m := anyHashPattern.FindStringSubmatchIndex(s)
if m == nil {
return ret, false
}
ret.PosStart, ret.PosEnd = m[0], m[1]
ret.FullURL = s[ret.PosStart:ret.PosEnd]
if strings.HasSuffix(ret.FullURL, ".") {
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
ret.PosEnd--
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
for i := 0; i < len(m); i++ {
m[i] = min(m[i], ret.PosEnd)
}
}
ret.CommitID = s[m[2]:m[3]]
if m[5] > 0 {
ret.SubPath = s[m[4]:m[5]]
}
lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
if lastEnd > 0 {
ret.QueryHash = s[lastStart:lastEnd][1:]
}
return ret, true
}
// fullHashPatternProcessor renders SHA containing URLs
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
return
}
next := node.NextSibling
for node != nil && node != next {
m := anyHashPattern.FindStringSubmatchIndex(node.Data)
if m == nil {
return
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue
}
urlFull := node.Data[m[0]:m[1]]
text := base.ShortSha(node.Data[m[2]:m[3]])
// 3rd capture group matches a optional path
subpath := ""
if m[5] > 0 {
subpath = node.Data[m[4]:m[5]]
ret, ok := anyHashPatternExtract(node.Data)
if !ok {
node = node.NextSibling
continue
}
// 4th capture group matches a optional url hash
hash := ""
if m[7] > 0 {
hash = node.Data[m[6]:m[7]][1:]
text := base.ShortSha(ret.CommitID)
if ret.SubPath != "" {
text += ret.SubPath
}
start := m[0]
end := m[1]
// If url ends in '.', it's very likely that it is not part of the
// actual url but used to finish a sentence.
if strings.HasSuffix(urlFull, ".") {
end--
urlFull = urlFull[:len(urlFull)-1]
if hash != "" {
hash = hash[:len(hash)-1]
} else if subpath != "" {
subpath = subpath[:len(subpath)-1]
}
if ret.QueryHash != "" {
text += " (" + ret.QueryHash + ")"
}
if subpath != "" {
text += subpath
}
if hash != "" {
text += " (" + hash + ")"
}
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
node = node.NextSibling.NextSibling
}
}
@ -1021,19 +1034,16 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
return
}
next := node.NextSibling
for node != nil && node != next {
m := comparePattern.FindStringSubmatchIndex(node.Data)
if m == nil {
return
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue
}
// Ensure that every group (m[0]...m[7]) has a match
for i := 0; i < 8; i++ {
if m[i] == -1 {
return
}
m := comparePattern.FindStringSubmatchIndex(node.Data)
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
node = node.NextSibling
continue
}
urlFull := node.Data[m[0]:m[1]]

View File

@ -60,7 +60,8 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
}
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
for node != nil {
nodeStop := node.NextSibling
for node != nodeStop {
if node.Type != html.TextNode {
node = node.NextSibling
continue

View File

@ -399,36 +399,61 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
}
func TestRegExp_anySHA1Pattern(t *testing.T) {
testCases := map[string][]string{
testCases := map[string]anyHashPatternResult{
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js",
"#L2703",
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
QueryHash: "L2703",
},
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
"a644101ed04d0beacea864ce805e0c4f86ba1cd1",
"/test/unit/event.js",
"",
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
SubPath: "/test/unit/event.js",
},
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
"0705be475092aede1eddae01319ec931fb9c65fc",
"",
"",
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
},
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
"0705be475092aede1eddae01319ec931fb9c65fc",
"/src",
"",
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
SubPath: "/src",
},
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
"d8a994ef243349f321568f9e36d5c3f444b99cae",
"",
"#diff-2",
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
QueryHash: "diff-2",
},
"non-url": {},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "L1-L2",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
SubPath: "/sub",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
},
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
QueryHash: "hash",
},
}
for k, v := range testCases {
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v)
ret, ok := anyHashPatternExtract(k)
if v.CommitID == "" {
assert.False(t, ok)
} else {
assert.EqualValues(t, strings.TrimSuffix(k, "."), ret.FullURL)
assert.EqualValues(t, v.CommitID, ret.CommitID)
assert.EqualValues(t, v.SubPath, ret.SubPath)
assert.EqualValues(t, v.QueryHash, ret.QueryHash)
}
}
}

View File

@ -124,6 +124,11 @@ func TestRender_CrossReferences(t *testing.T) {
test(
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
test(
inputURL,
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
}
func TestMisc_IsSameDomain(t *testing.T) {
@ -695,7 +700,7 @@ func TestIssue18471(t *testing.T) {
}, strings.NewReader(data), &res)
assert.NoError(t, err)
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
}
func TestIsFullURL(t *testing.T) {

32
modules/setting/glob.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import "github.com/gobwas/glob"
type GlobMatcher struct {
compiledGlob glob.Glob
patternString string
}
var _ glob.Glob = (*GlobMatcher)(nil)
func (g *GlobMatcher) Match(s string) bool {
return g.compiledGlob.Match(s)
}
func (g *GlobMatcher) PatternString() string {
return g.patternString
}
func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return nil, err
}
return &GlobMatcher{
compiledGlob: g,
patternString: pattern,
}, nil
}

View File

@ -10,8 +10,6 @@ import (
"time"
"code.gitea.io/gitea/modules/log"
"github.com/gobwas/glob"
)
// Indexer settings
@ -30,8 +28,8 @@ var Indexer = struct {
RepoConnStr string
RepoIndexerName string
MaxIndexerFileSize int64
IncludePatterns []glob.Glob
ExcludePatterns []glob.Glob
IncludePatterns []*GlobMatcher
ExcludePatterns []*GlobMatcher
ExcludeVendored bool
}{
IssueType: "bleve",
@ -93,12 +91,12 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
}
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
func IndexerGlobFromString(globstr string) []glob.Glob {
extarr := make([]glob.Glob, 0, 10)
func IndexerGlobFromString(globstr string) []*GlobMatcher {
extarr := make([]*GlobMatcher, 0, 10)
for _, expr := range strings.Split(strings.ToLower(globstr), ",") {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil {
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else {
extarr = append(extarr, g)

View File

@ -17,6 +17,16 @@ import (
const tplSearch base.TplName = "repo/search"
func indexSettingToGitGrepPathspecList() (list []string) {
for _, expr := range setting.Indexer.IncludePatterns {
list = append(list, ":(glob)"+expr.PatternString())
}
for _, expr := range setting.Indexer.ExcludePatterns {
list = append(list, ":(glob,exclude)"+expr.PatternString())
}
return list
}
// Search render repository search page
func Search(ctx *context.Context) {
language := ctx.FormTrim("l")
@ -65,8 +75,14 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
}
} else {
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy})
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: isFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil {
// TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree.
ctx.ServerError("GrepSearch", err)
return
}

View File

@ -0,0 +1,19 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestIndexSettingToGitGrepPathspecList(t *testing.T) {
defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))()
defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))()
assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList())
}

View File

@ -157,168 +157,171 @@
<!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<!-- Email -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}">
{{ctx.Locale.Tr "install.email_title"}}
</summary>
<div class="inline field">
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label>
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}">
</div>
<div class="inline field">
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label>
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}">
</div>
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}">
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label>
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}">
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span>
</div>
<div class="inline field {{if .Err_SMTPUser}}error{{end}}">
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label>
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}">
</div>
<div class="inline field">
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label>
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}">
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label>
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}>
<div>
<!-- Email -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}">
{{ctx.Locale.Tr "install.email_title"}}
</summary>
<div class="inline field">
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label>
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}">
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label>
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}>
<div class="inline field">
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label>
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}">
</div>
</div>
</details>
<!-- Server and other services -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}">
{{ctx.Locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
<div class="ui checkbox" id="offline-mode">
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label>
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}>
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}">
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label>
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}">
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-gravatar">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label>
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}>
<div class="inline field {{if .Err_SMTPUser}}error{{end}}">
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label>
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}">
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="federated-avatar-lookup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label>
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}>
<div class="inline field">
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label>
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}">
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label>
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="allow-only-external-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label>
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label>
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-captcha">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label>
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label>
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label>
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}>
</div>
</div>
<div class="inline field">
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label>
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}">
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span>
</div>
<div class="inline field">
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label>
<div class="ui selection dropdown">
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}">
<div class="text">{{.password_algorithm}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range .PasswordHashAlgorithms}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label>
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}>
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
</div>
</details>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label>
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}>
</div>
</div>
</details>
<!-- Admin -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}}
</summary>
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
<div class="inline field {{if .Err_AdminName}}error{{end}}">
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label>
<input id="admin_name" name="admin_name" value="{{.admin_name}}">
</div>
<div class="inline field {{if .Err_AdminEmail}}error{{end}}">
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label>
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label>
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label>
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}">
</div>
</details>
<!-- Server and other services -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}">
{{ctx.Locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
<div class="ui checkbox" id="offline-mode">
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label>
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-gravatar">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label>
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="federated-avatar-lookup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label>
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signin">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label>
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="disable-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label>
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="allow-only-external-registration">
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label>
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-openid-signup">
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label>
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox" id="enable-captcha">
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label>
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label>
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label>
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label>
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}>
</div>
</div>
<div class="inline field">
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label>
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}">
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span>
</div>
<div class="inline field">
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label>
<div class="ui selection dropdown">
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}">
<div class="text">{{.password_algorithm}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range .PasswordHashAlgorithms}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
</div>
</details>
<!-- Admin -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}}
</summary>
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
<div class="inline field {{if .Err_AdminName}}error{{end}}">
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label>
<input id="admin_name" name="admin_name" value="{{.admin_name}}">
</div>
<div class="inline field {{if .Err_AdminEmail}}error{{end}}">
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label>
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label>
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label>
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}">
</div>
</details>
</div>
<div class="divider"></div>
{{if .EnvConfigKeys}}
<!-- Environment Config -->
@ -333,12 +336,11 @@
</div>
{{end}}
<div class="divider"></div>
<div class="inline field">
<div class="right-content">
These configuration options will be written into: {{.CustomConfFile}}
</div>
<div class="right-content tw-mt-2">
<div class="tw-mt-4 tw-mb-2 tw-text-center">
<button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button>
</div>
</div>

View File

@ -4,29 +4,36 @@
</div>
{{end}}
<div class="issue-title-header">
<div class="issue-title" id="issue-title-wrapper">
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="issue-title" id="issue-title-display">
<h1 class="gt-word-break">
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
</span>
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden">
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
<span class="index">#{{.Issue.Index}}</span>
</h1>
<div class="issue-title-buttons">
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{if $canEditIssueTitle}}
<button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button>
{{end}}
{{if not .Issue.IsPull}}
<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
<a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
{{end}}
</div>
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="edit-buttons">
<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>
{{end}}
</div>
{{if $canEditIssueTitle}}
<div class="ui form issue-title tw-hidden" id="issue-title-editor">
<div class="ui input tw-flex-1">
<input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off">
</div>
<div class="issue-title-buttons">
<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui small primary button"
data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
{{ctx.Locale.Tr "repo.issues.save"}}
</button>
</div>
</div>
{{end}}
<div class="issue-title-meta">
{{if .HasMerged}}
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
@ -63,14 +70,14 @@
{{end}}
{{else}}
{{if .Issue.OriginalAuthor}}
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span>
{{else}}
<span id="pull-desc" class="pull-desc">
<span id="pull-desc-display" class="pull-desc">
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
</span>
{{end}}
<span id="pull-desc-edit" class="tw-hidden flex-text-block">
<span id="pull-desc-editor" class="tw-hidden flex-text-block">
<div class="ui floating filter dropdown">
<div class="ui basic small button tw-mr-0">
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span>

View File

@ -195,14 +195,17 @@ func TestAPIEditUser(t *testing.T) {
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2")
fullNameToChange := "Full Name User 2"
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
// required
"login_name": "user2",
"source_id": "0",
// to change
"full_name": "Full Name User 2",
"full_name": fullNameToChange,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.Equal(t, fullNameToChange, user2.FullName)
empty := ""
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
@ -216,7 +219,7 @@ func TestAPIEditUser(t *testing.T) {
json.Unmarshal(resp.Body.Bytes(), &errMap)
assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
assert.False(t, user2.IsRestricted)
bTrue := true
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{

View File

@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find("#issue-title").Text()
val := htmlDoc.doc.Find("#issue-title-display").Text()
assert.Contains(t, val, title)
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
assert.Equal(t, content, val)

View File

@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) {
req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{

View File

@ -13,8 +13,7 @@
.page-content.install .ui.form .field > .help,
.page-content.install .ui.form .field > .ui.checkbox:first-child,
.page-content.install .ui.form .field > .right-content {
margin-left: 30%;
padding-left: 5px;
margin-left: calc(30% + 5px);
width: auto;
}
@ -24,10 +23,11 @@
}
.page-content.install form.ui.form details.optional.field[open] {
border-bottom: 1px dashed var(--color-secondary);
padding-bottom: 10px;
}
.page-content.install form.ui.form details.optional.field[open]:not(:last-child) {
border-bottom: 1px dashed var(--color-secondary);
}
.page-content.install form.ui.form details.optional.field[open] summary {
margin-bottom: 10px;
}

View File

@ -41,7 +41,7 @@ input[type="radio"] {
.ui.checkbox label,
.ui.radio.checkbox label {
margin-left: 1.85714em;
margin-left: 20px;
}
.ui.checkbox + label {

View File

@ -575,34 +575,7 @@ td .commit-summary {
display: inline-block;
}
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
display: flex;
}
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons,
.repository.view.issue .edit-buttons {
width: 100%;
justify-content: space-between;
}
.repository.view.issue .edit-buttons {
margin-top: .5rem;
}
.comment.form .issue-content-left .avatar {
display: none;
}
@ -617,15 +590,37 @@ td .commit-summary {
}
}
/* issue title & meta & edit */
.issue-title-header {
width: 100%;
padding-bottom: 4px;
margin-bottom: 1rem;
}
.issue-title-meta {
display: flex;
align-items: center;
}
.repository.view.issue .issue-title-buttons {
display: flex;
gap: 0.5em;
}
.repository.view.issue .issue-title-buttons > .ui.button {
margin: 0;
height: 35px;
}
.repository.view.issue .issue-title {
display: flex;
align-items: center;
gap: 0.5em;
margin-bottom: 8px;
min-height: 40px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 {
display: flex;
align-items: center;
flex: 1;
width: 100%;
font-weight: var(--font-weight-normal);
@ -633,14 +628,24 @@ td .commit-summary {
line-height: 40px;
margin: 0;
padding-right: 0.25rem;
min-height: 41px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 .ui.input {
font-size: 0.5em;
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
}
.repository.view.issue .issue-title-buttons {
width: 100%;
justify-content: space-between;
}
}
.repository.view.issue .issue-title h1 .ui.input input {
.repository.view.issue .issue-title .ui.input {
width: 100%;
height: 35px;
}
.repository.view.issue .issue-title .ui.input input {
font-size: 1.5em;
padding: 2px .5rem;
}
@ -653,10 +658,6 @@ td .commit-summary {
margin-right: 10px;
}
.issue-title .edit-zone {
margin-top: 10px;
}
.issue-state-label {
display: flex !important;
align-items: center !important;

View File

@ -6,13 +6,20 @@
// This file must be imported before any lazy-loading is being attempted.
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
export function showGlobalErrorMessage(msg) {
const pageContent = document.querySelector('.page-content');
if (!pageContent) return;
function shouldIgnoreError(err) {
const ignorePatterns = [
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
];
for (const pattern of ignorePatterns) {
if (err.stack?.includes(pattern)) return true;
}
return false;
}
// compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = msg.replace(/\W/g, '').trim();
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
export function showGlobalErrorMessage(msg) {
const msgContainer = document.querySelector('.page-content') ?? document.body;
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
if (!msgDiv) {
const el = document.createElement('div');
el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
@ -23,7 +30,7 @@ export function showGlobalErrorMessage(msg) {
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
pageContent.prepend(msgDiv);
msgContainer.prepend(msgDiv);
}
/**
@ -52,10 +59,12 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno
if (runModeIsProd) return;
}
// If the error stack trace does not include the base URL of our script assets, it likely came
// from a browser extension or inline script. Do not show such errors in production.
if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) {
return;
if (err instanceof Error) {
// If the error stack trace does not include the base URL of our script assets, it likely came
// from a browser extension or inline script. Do not show such errors in production.
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
// Ignore some known errors that are unable to fix
if (shouldIgnoreError(err)) return;
}
let msg = err?.message ?? message;

View File

@ -67,7 +67,7 @@ export default {
const weekValues = Object.values(this.data);
const start = weekValues[0].week;
const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(start), new Date(end));
const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
this.errorText = '';
} else {

View File

@ -114,7 +114,7 @@ export default {
const weekValues = Object.values(total.weeks);
this.xAxisStart = weekValues[0].week;
this.xAxisEnd = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
this.xAxisMin = this.xAxisStart;
this.xAxisMax = this.xAxisEnd;

View File

@ -62,7 +62,7 @@ export default {
const data = await response.json();
const start = Object.values(data)[0].week;
const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(start), new Date(end));
const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
this.errorText = '';
} else {

View File

@ -47,10 +47,18 @@ export function initFootLanguageMenu() {
export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => {
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter');
if (isQuickSubmitEnter && e.target.matches('textarea')) {
e.preventDefault();
handleGlobalEnterQuickSubmit(e.target);
if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) {
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
} else if (e.target.matches('input') && !e.target.closest('form')) {
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
// eslint-disable-next-line unicorn/no-lonely-if
if (handleGlobalEnterQuickSubmit(e.target)) {
e.preventDefault();
}
}
});
}

View File

@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) {
if (form) {
if (!form.checkValidity()) {
form.reportValidity();
return;
} else {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
}
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
return;
return true;
}
form = target.closest('.ui.form');
if (form) {
form.querySelector('.ui.primary.button')?.click();
return true;
}
return false;
}

View File

@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd
import {toAbsoluteUrl} from '../utils.js';
import {initDropzone} from './common-global.js';
import {POST, GET} from '../modules/fetch.js';
import {showErrorToast} from '../modules/toast.js';
const {appSubUrl} = window.config;
@ -298,23 +299,23 @@ export function initRepoPullRequestMergeInstruction() {
export function initRepoPullRequestAllowMaintainerEdit() {
const wrapper = document.getElementById('allow-edits-from-maintainers');
if (!wrapper) return;
wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => {
const checked = e.target.checked;
const checkbox = wrapper.querySelector('input[type="checkbox"]');
checkbox.addEventListener('input', async () => {
const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
wrapper.classList.add('is-loading');
e.target.disabled = true;
try {
const response = await POST(url, {data: {allow_maintainer_edit: checked}});
if (!response.ok) {
const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})});
if (!resp.ok) {
throw new Error('Failed to update maintainer edit permission');
}
const data = await resp.json();
checkbox.checked = data.allow_maintainer_edit;
} catch (error) {
checkbox.checked = !checkbox.checked;
console.error(error);
showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
} finally {
wrapper.classList.remove('is-loading');
e.target.disabled = false;
}
});
}
@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() {
});
}
async function pullrequest_targetbranch_change(update_url) {
const targetBranch = $('#pull-target-branch').data('branch');
const $branchTarget = $('#branch_target');
if (targetBranch === $branchTarget.text()) {
window.location.reload();
return false;
}
try {
await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})});
} catch (error) {
console.error(error);
} finally {
window.location.reload();
}
}
export function initRepoIssueTitleEdit() {
// Edit issue title
const $issueTitle = $('#issue-title');
const $editInput = $('#edit-title-input input');
const issueTitleDisplay = document.querySelector('#issue-title-display');
const issueTitleEditor = document.querySelector('#issue-title-editor');
if (!issueTitleEditor) return;
const editTitleToggle = function () {
toggleElem($issueTitle);
toggleElem('.not-in-edit');
toggleElem('#edit-title-input');
toggleElem('#pull-desc');
toggleElem('#pull-desc-edit');
toggleElem('.in-edit');
toggleElem('.new-issue-button');
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active');
$editInput[0].focus();
$editInput[0].select();
return false;
};
$('#edit-title').on('click', editTitleToggle);
$('#cancel-edit-title').on('click', editTitleToggle);
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () {
const pullrequest_target_update_url = this.getAttribute('data-target-update-url');
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) {
$editInput.val($issueTitle.text());
await pullrequest_targetbranch_change(pullrequest_target_update_url);
} else {
try {
const params = new URLSearchParams();
params.append('title', $editInput.val());
const response = await POST(this.getAttribute('data-update-url'), {data: params});
const data = await response.json();
$editInput.val(data.title);
$issueTitle.text(data.title);
if (pullrequest_target_update_url) {
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window
} else {
window.location.reload();
}
} catch (error) {
console.error(error);
}
const issueTitleInput = issueTitleEditor.querySelector('input');
const oldTitle = issueTitleInput.getAttribute('data-old-title');
issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
hideElem(issueTitleDisplay);
hideElem('#pull-desc-display');
showElem(issueTitleEditor);
showElem('#pull-desc-editor');
if (!issueTitleInput.value.trim()) {
issueTitleInput.value = oldTitle;
}
issueTitleInput.focus();
});
issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
hideElem(issueTitleEditor);
hideElem('#pull-desc-editor');
showElem(issueTitleDisplay);
showElem('#pull-desc-display');
});
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
editSaveButton.addEventListener('click', async () => {
const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
const newTitle = issueTitleInput.value.trim();
try {
if (newTitle && newTitle !== oldTitle) {
const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
if (!resp.ok) {
throw new Error(`Failed to update issue title: ${resp.statusText}`);
}
}
if (prTargetUpdateUrl) {
const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
const oldTargetBranch = document.querySelector('#branch_target').textContent;
if (newTargetBranch !== oldTargetBranch) {
const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
if (!resp.ok) {
throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
}
}
}
window.location.reload();
} catch (error) {
console.error(error);
showErrorToast(error.message);
}
return false;
});
}
export function initRepoIssueBranchSelect() {
const changeBranchSelect = function () {
const $selectionTextField = $('#pull-target-branch');
const baseName = $selectionTextField.data('basename');
const branchNameNew = $(this).data('branch');
const branchNameOld = $selectionTextField.data('branch');
// Replace branch name to keep translation from HTML template
$selectionTextField.html($selectionTextField.html().replace(
`${baseName}:${branchNameOld}`,
`${baseName}:${branchNameNew}`,
));
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
};
$('#branch-select > .item').on('click', changeBranchSelect);
document.querySelector('#branch-select')?.addEventListener('click', (e) => {
const el = e.target.closest('.item[data-branch]');
if (!el) return;
const pullTargetBranch = document.querySelector('#pull-target-branch');
const baseName = pullTargetBranch.getAttribute('data-basename');
const branchNameNew = el.getAttribute('data-branch');
const branchNameOld = pullTargetBranch.getAttribute('data-branch');
pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
pullTargetBranch.setAttribute('data-branch', branchNameNew);
});
}
export function initSingleCommentEditor($commentForm) {

View File

@ -1,25 +1,30 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import {getCurrentLocale} from '../utils.js';
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
export function startDaysBetween(startDate, endDate) {
// Ensure the start date is a Sunday
while (startDate.getDay() !== 0) {
startDate.setDate(startDate.getDate() + 1);
}
dayjs.extend(utc);
const start = dayjs(startDate);
const end = dayjs(endDate);
const startDays = [];
/**
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
*
* @param startConfig The start date. Can take any type that `Date` accepts.
* @param endConfig The end date. Can take any type that `Date` accepts.
*/
export function startDaysBetween(startDate, endDate) {
const start = dayjs.utc(startDate);
const end = dayjs.utc(endDate);
let current = start;
// Ensure the start date is a Sunday
while (current.day() !== 0) {
current = current.add(1, 'day');
}
const startDays = [];
while (current.isBefore(end)) {
startDays.push(current.valueOf());
// we are adding 7 * 24 hours instead of 1 week because we don't want
// date library to use local time zone to calculate 1 week from now.
// local time zone is problematic because of daylight saving time (dst)
// used on some countries
current = current.add(7 * 24, 'hour');
current = current.add(1, 'week');
}
return startDays;
@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) {
if (!(inputDate instanceof Date)) {
throw new Error('Invalid date');
}
const dayOfWeek = inputDate.getDay();
const dayOfWeek = inputDate.getUTCDay();
const daysUntilSunday = 7 - dayOfWeek;
const resultDate = new Date(inputDate.getTime());
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
return resultDate.valueOf();
}