Compare commits

...

14 Commits

Author SHA1 Message Date
silverwind 43aa914b17
fix double border and border-radius on empty action steps (#29845)
Before, double border-bottom and incorrect border-radius:

<img width="914" alt="Screenshot 2024-03-16 at 14 46 31"
src="https://github.com/go-gitea/gitea/assets/115237/6ea63c42-754c-420c-a0f5-c889a8507d9f">

After, both fixed:

<img width="917" alt="Screenshot 2024-03-16 at 14 45 59"
src="https://github.com/go-gitea/gitea/assets/115237/9d3f2dba-6b22-441d-8e99-5809d5f1f1c0">
2024-03-16 17:03:56 +00:00
silverwind ffeaf2d0bd
add `.suppressed` link class (#29847)
Extract from https://github.com/go-gitea/gitea/pull/29344. With this
class it's possible to have links that don't color on hover. It will be
useful for https://github.com/go-gitea/gitea/pull/29429.
2024-03-16 17:58:58 +01:00
silverwind 21fe512aac
Forbid jQuery `.prop` and fix related issues (#29832)
The issue checkbox code received a few more cleanups and I specifically
tested it. The other changes are trivial. Also, I checked the cases for
how many elements match the jQuery selection to determine querySelector
vs. querySelectorAll.

---------

Co-authored-by: Giteabot <teabot@gitea.io>
2024-03-16 15:08:10 +00:00
Lunny Xiao cb78648ad9
Fix wrong test for TestPullView_CodeOwner (#29838)
It's weird the previous test was PASS.
2024-03-16 13:59:02 +00:00
Yarden Shoham f9b4efd42c
Forbid HTML injection using jQuery (#29843)
See
https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/no-append-html.md

Tested the following components and they work as before:
- notification table
- issue author dropdown
- comment edit box attachments div

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2024-03-16 13:25:27 +00:00
6543 c6e5ec51bd
Meilisearch double quote on "match" query (#29740)
make `nonFuzzyWorkaround` unessesary

cc @Kerollmops
2024-03-16 13:19:41 +00:00
Yarden Shoham 3cd64949ae
Forbid variables containing jQuery collections not having the `$` prefix (#29839)
See
https://github.com/wikimedia/eslint-plugin-no-jquery/blob/master/docs/rules/variable-pattern.md

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
2024-03-16 12:22:16 +00:00
wxiaoguang a889381664
Remove AddParamIfExist(AddParam) (#29841)
Follow #29834

Remove AddParamIfExist, use "AddParamString" instead, it should clearly
know what is being added into the parameters.
2024-03-16 12:07:56 +00:00
wxiaoguang 66902d89e5
Refactor markdown attention render (#29833)
* Remove some deadcode
* Use 2-word name for CSS class names
* Remove "gt-*" rules for sanitizer

The UI doesn't change much.
2024-03-16 11:34:38 +00:00
6543 1262ff6734
Refactor code_indexer to use an SearchOptions struct for PerformSearch (#29724)
similar to how it's already done for the issue_indexer


---
*Sponsored by Kithara Software GmbH*
2024-03-16 10:32:45 +00:00
wxiaoguang e0ea3811c4
Refactor AddParam to AddParamIfExist (#29834)
When read the code: `pager.AddParam(ctx, "search", "search")`, the
question always comes: What is it doing? Where is the value from? Why
"search" / "search" ?

Now it is clear: `pager.AddParamIfExist("search", ctx.Data["search"])`
2024-03-16 10:20:13 +01:00
Yarden Shoham 6ead30dbc4
Forbid jQuery AJAX (#29818)
Please use the fetch wrapper instead, or even better `htmx`.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: 6543 <6543@obermui.de>
2024-03-16 02:01:25 +00:00
Yarden Shoham 043f55fabf
Remove jQuery AJAX from the notifications (#29817)
- Removed 2 jQuery AJAX calls and replaced with our fetch wrapper
- Deleted an AJAX call that wasn't attached to any element since #24989
- Tested the notification count and notification table functionality and
it works as before

# Demo using `fetch` instead of jQuery AJAX

![demo](https://github.com/go-gitea/gitea/assets/20454870/ff862a9a-1c88-41cc-bd01-5a0711dbd6f8)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
2024-03-16 02:56:17 +01:00
silverwind 68169133a3
Light theme color enhancements (#29830)
Same as https://github.com/go-gitea/gitea/pull/29822 but for light
theme. Slight shift towards blue and made the themes match more, like on
header and footer background.

Before
<img width="1342" alt="Screenshot 2024-03-16 at 00 43 03"
src="https://github.com/go-gitea/gitea/assets/115237/b46021a1-241c-446a-b220-ca25cc90f3bf">


After
<img width="1343" alt="Screenshot 2024-03-16 at 00 45 21"
src="https://github.com/go-gitea/gitea/assets/115237/1c898875-a6bb-4bd3-b059-f82e1a145c99">


Before
<img width="1018" alt="Screenshot 2024-03-16 at 00 43 13"
src="https://github.com/go-gitea/gitea/assets/115237/d237ee7d-b4cc-4688-a074-1e96515ac475">

After
<img width="1022" alt="Screenshot 2024-03-16 at 00 43 50"
src="https://github.com/go-gitea/gitea/assets/115237/89b1da77-6bc9-4b38-9688-546e794aadfa">
2024-03-16 02:33:01 +01:00
49 changed files with 560 additions and 566 deletions

View File

@ -283,7 +283,7 @@ rules:
i/unambiguous: [0]
init-declarations: [0]
jquery/no-ajax-events: [2]
jquery/no-ajax: [0]
jquery/no-ajax: [2]
jquery/no-animate: [2]
jquery/no-attr: [0]
jquery/no-bind: [2]
@ -315,7 +315,7 @@ rules:
jquery/no-parent: [0]
jquery/no-parents: [0]
jquery/no-parse-html: [2]
jquery/no-prop: [0]
jquery/no-prop: [2]
jquery/no-proxy: [2]
jquery/no-ready: [2]
jquery/no-serialize: [2]
@ -396,11 +396,11 @@ rules:
no-irregular-whitespace: [2]
no-iterator: [2]
no-jquery/no-ajax-events: [2]
no-jquery/no-ajax: [0]
no-jquery/no-ajax: [2]
no-jquery/no-and-self: [2]
no-jquery/no-animate-toggle: [2]
no-jquery/no-animate: [2]
no-jquery/no-append-html: [0]
no-jquery/no-append-html: [2]
no-jquery/no-attr: [0]
no-jquery/no-bind: [2]
no-jquery/no-box-model: [2]
@ -466,7 +466,7 @@ rules:
no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2]
no-jquery/no-prop: [0]
no-jquery/no-prop: [2]
no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2]
@ -487,7 +487,7 @@ rules:
no-jquery/no-visibility: [2]
no-jquery/no-when: [2]
no-jquery/no-wrap: [2]
no-jquery/variable-pattern: [0]
no-jquery/variable-pattern: [2]
no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2]

View File

@ -142,7 +142,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
return err
}
if size, err = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil {
return fmt.Errorf("Misformatted git cat-file output: %w", err)
return fmt.Errorf("misformatted git cat-file output: %w", err)
}
}
@ -233,26 +233,26 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error {
// Search searches for files in the specified repo.
// Returns the matching file-paths
func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
var (
indexerQuery query.Query
keywordQuery query.Query
)
if isFuzzy {
phraseQuery := bleve.NewMatchPhraseQuery(keyword)
if opts.IsKeywordFuzzy {
phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
phraseQuery.FieldVal = "Content"
phraseQuery.Analyzer = repoIndexerAnalyzer
keywordQuery = phraseQuery
} else {
prefixQuery := bleve.NewPrefixQuery(keyword)
prefixQuery := bleve.NewPrefixQuery(opts.Keyword)
prefixQuery.FieldVal = "Content"
keywordQuery = prefixQuery
}
if len(repoIDs) > 0 {
repoQueries := make([]query.Query, 0, len(repoIDs))
for _, repoID := range repoIDs {
if len(opts.RepoIDs) > 0 {
repoQueries := make([]query.Query, 0, len(opts.RepoIDs))
for _, repoID := range opts.RepoIDs {
repoQueries = append(repoQueries, inner_bleve.NumericEqualityQuery(repoID, "RepoID"))
}
@ -266,8 +266,8 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
// Save for reuse without language filter
facetQuery := indexerQuery
if len(language) > 0 {
languageQuery := bleve.NewMatchQuery(language)
if len(opts.Language) > 0 {
languageQuery := bleve.NewMatchQuery(opts.Language)
languageQuery.FieldVal = "Language"
languageQuery.Analyzer = analyzer_keyword.Name
@ -277,12 +277,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
)
}
from := (page - 1) * pageSize
from, pageSize := opts.GetSkipTake()
searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
searchRequest.IncludeLocations = true
if len(language) == 0 {
if len(opts.Language) == 0 {
searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
}
@ -326,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
}
searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10)
if len(language) > 0 {
if len(opts.Language) > 0 {
// Use separate query to go get all language counts
facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false)
facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}

View File

@ -281,18 +281,18 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan
}
// Search searches for codes and language stats by given conditions.
func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
searchType := esMultiMatchTypePhrasePrefix
if isFuzzy {
if opts.IsKeywordFuzzy {
searchType = esMultiMatchTypeBestFields
}
kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType)
kwQuery := elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType)
query := elastic.NewBoolQuery()
query = query.Must(kwQuery)
if len(repoIDs) > 0 {
repoStrs := make([]any, 0, len(repoIDs))
for _, repoID := range repoIDs {
if len(opts.RepoIDs) > 0 {
repoStrs := make([]any, 0, len(opts.RepoIDs))
for _, repoID := range opts.RepoIDs {
repoStrs = append(repoStrs, repoID)
}
repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...)
@ -300,16 +300,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
}
var (
start int
kw = "<em>" + keyword + "</em>"
aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
start, pageSize = opts.GetSkipTake()
kw = "<em>" + opts.Keyword + "</em>"
aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc()
)
if page > 0 {
start = (page - 1) * pageSize
}
if len(language) == 0 {
if len(opts.Language) == 0 {
searchResult, err := b.inner.Client.Search().
Index(b.inner.VersionedIndexName()).
Aggregation("language", aggregation).
@ -330,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword
return convertResult(searchResult, kw, pageSize)
}
langQuery := elastic.NewMatchQuery("language", language)
langQuery := elastic.NewMatchQuery("language", opts.Language)
countResult, err := b.inner.Client.Search().
Index(b.inner.VersionedIndexName()).
Aggregation("language", aggregation).

View File

@ -32,7 +32,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s
needGenesis := len(status.CommitSha) == 0
if !needGenesis {
hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision)
hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(status.CommitSha, revision)
stdout, _, _ := hasAncestorCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
needGenesis = len(stdout) == 0
}

View File

@ -8,6 +8,7 @@ import (
"os"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/indexer/code/bleve"
@ -70,7 +71,15 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
for _, kw := range keywords {
t.Run(kw.Keyword, func(t *testing.T) {
total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, true)
total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{
RepoIDs: kw.RepoIDs,
Keyword: kw.Keyword,
Paginator: &db.ListOptions{
Page: 1,
PageSize: 10,
},
IsKeywordFuzzy: true,
})
assert.NoError(t, err)
assert.Len(t, kw.IDs, int(total))
assert.Len(t, langs, kw.Langs)

View File

@ -7,6 +7,7 @@ import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/indexer/internal"
)
@ -16,7 +17,17 @@ type Indexer interface {
internal.Indexer
Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error
Delete(ctx context.Context, repoID int64) error
Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error)
Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error)
}
type SearchOptions struct {
RepoIDs []int64
Keyword string
Language string
IsKeywordFuzzy bool
db.Paginator
}
// NewDummyIndexer returns a dummy indexer
@ -38,6 +49,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error {
return fmt.Errorf("indexer is not ready")
}
func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int64, []*SearchResult, []*SearchResultLanguages, error) {
func (d *dummyIndexer) Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) {
return 0, nil, nil, fmt.Errorf("indexer is not ready")
}

View File

@ -32,6 +32,8 @@ type ResultLine struct {
type SearchResultLanguages = internal.SearchResultLanguages
type SearchOptions = internal.SearchOptions
func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) {
startIndex := selectionStartIndex
numLinesBefore := 0
@ -125,12 +127,12 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
// PerformSearch perform a search on a repository
// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2
func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isFuzzy bool) (int, []*Result, []*internal.SearchResultLanguages, error) {
if len(keyword) == 0 {
func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) {
if opts == nil || len(opts.Keyword) == 0 {
return 0, nil, nil, nil
}
total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isFuzzy)
total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts)
if err != nil {
return 0, nil, nil, err
}

View File

@ -6,6 +6,7 @@ package meilisearch
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
@ -217,7 +218,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits)
searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(options.Keyword, &meilisearch.SearchRequest{
keyword := options.Keyword
if !options.IsFuzzyKeyword {
// to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s)
// https://www.meilisearch.com/docs/reference/api/search#phrase-search
keyword = doubleQuoteKeyword(keyword)
}
searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{
Filter: query.Statement(),
Limit: int64(limit),
Offset: int64(skip),
@ -228,7 +236,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
return nil, err
}
hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword)
hits, err := convertHits(searchRes)
if err != nil {
return nil, err
}
@ -247,11 +255,20 @@ func parseSortBy(sortBy internal.SortBy) string {
return field + ":asc"
}
// nonFuzzyWorkaround is needed as meilisearch does not have an exact search
// and you can only change "typo tolerance" per index. So we have to post-filter the results
// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance
// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed
func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) {
func doubleQuoteKeyword(k string) string {
kp := strings.Split(k, " ")
parts := 0
for i := range kp {
part := strings.Trim(kp[i], "\"")
if part != "" {
kp[parts] = fmt.Sprintf(`"%s"`, part)
parts++
}
}
return strings.Join(kp[:parts], " ")
}
func convertHits(searchRes *meilisearch.SearchResponse) ([]internal.Match, error) {
hits := make([]internal.Match, 0, len(searchRes.Hits))
for _, hit := range searchRes.Hits {
hit, ok := hit.(map[string]any)
@ -259,61 +276,11 @@ func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, i
return nil, ErrMalformedResponse
}
if !isFuzzy {
keyword = strings.ToLower(keyword)
// declare a anon func to check if the title, content or at least one comment contains the keyword
found, err := func() (bool, error) {
// check if title match first
title, ok := hit["title"].(string)
if !ok {
return false, ErrMalformedResponse
} else if strings.Contains(strings.ToLower(title), keyword) {
return true, nil
}
// check if content has a match
content, ok := hit["content"].(string)
if !ok {
return false, ErrMalformedResponse
} else if strings.Contains(strings.ToLower(content), keyword) {
return true, nil
}
// now check for each comment if one has a match
// so we first try to cast and skip if there are no comments
comments, ok := hit["comments"].([]any)
if !ok {
return false, ErrMalformedResponse
} else if len(comments) == 0 {
return false, nil
}
// now we iterate over all and report as soon as we detect one match
for i := range comments {
comment, ok := comments[i].(string)
if !ok {
return false, ErrMalformedResponse
}
if strings.Contains(strings.ToLower(comment), keyword) {
return true, nil
}
}
// we got no match
return false, nil
}()
if err != nil {
return nil, err
} else if !found {
continue
}
}
issueID, ok := hit["id"].(float64)
if !ok {
return nil, ErrMalformedResponse
}
hits = append(hits, internal.Match{
ID: int64(issueID),
})

View File

@ -53,11 +53,10 @@ func TestMeilisearchIndexer(t *testing.T) {
tests.TestIndexer(t, indexer)
}
func TestNonFuzzyWorkaround(t *testing.T) {
// get unexpected return
_, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{
func TestConvertHits(t *testing.T) {
_, err := convertHits(&meilisearch.SearchResponse{
Hits: []any{"aa", "bb", "cc", "dd"},
}, "bowling", false)
})
assert.ErrorIs(t, err, ErrMalformedResponse)
validResponse := &meilisearch.SearchResponse{
@ -82,14 +81,15 @@ func TestNonFuzzyWorkaround(t *testing.T) {
},
},
}
// nonFuzzy
hits, err := nonFuzzyWorkaround(validResponse, "bowling", false)
assert.NoError(t, err)
assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits)
// fuzzy
hits, err = nonFuzzyWorkaround(validResponse, "bowling", true)
hits, err := convertHits(validResponse)
assert.NoError(t, err)
assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
}
func TestDoubleQuoteKeyword(t *testing.T) {
assert.EqualValues(t, "", doubleQuoteKeyword(""))
assert.EqualValues(t, `"a" "b" "c"`, doubleQuoteKeyword("a b c"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword(`a "" "d" """g`))
}

View File

@ -175,13 +175,6 @@ func NewColorPreview(color []byte) *ColorPreview {
}
}
// IsColorPreview returns true if the given node implements the ColorPreview interface,
// otherwise false.
func IsColorPreview(node ast.Node) bool {
_, ok := node.(*ColorPreview)
return ok
}
// Attention is an inline for an attention
type Attention struct {
ast.BaseInline

View File

@ -216,7 +216,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
attentionType := strings.ToLower(strings.TrimPrefix(string(secondTextNode.Segment.Value(reader.Source())), "!"))
// color the blockquote
v.SetAttributeString("class", []byte("gt-py-3 attention attention-"+attentionType))
v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
// create an emphasis to make it bold
attentionParagraph := ast.NewParagraph()
@ -364,27 +364,21 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
_, _ = w.WriteString(`<span class="gt-mr-2 gt-vm attention-`)
n := node.(*Attention)
_, _ = w.WriteString(strings.ToLower(n.AttentionType))
_, _ = w.WriteString(`">`)
var octiconType string
var octiconName string
switch n.AttentionType {
case "note":
octiconType = "info"
case "tip":
octiconType = "light-bulb"
octiconName = "light-bulb"
case "important":
octiconType = "report"
octiconName = "report"
case "warning":
octiconType = "alert"
octiconName = "alert"
case "caution":
octiconType = "stop"
octiconName = "stop"
default: // including "note"
octiconName = "info"
}
_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
} else {
_, _ = w.WriteString("</span>\n")
_, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
}
return ast.WalkContinue, nil
}

View File

@ -64,10 +64,9 @@ func createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
// For attention
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-py-3 attention attention-\w+$`)).OnElements("blockquote")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^gt-mr-2 gt-vm attention-\w+$`)).OnElements("span", "strong")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-(\w|-)+$`)).OnElements("svg")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
policy.AllowAttrs("fill-rule", "d").OnElements("path")
@ -105,18 +104,12 @@ func createDefaultPolicy() *bluemonday.Policy {
// Allow icons
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
// Allow unlabelled labels
policy.AllowNoAttrs().OnElements("label")
// Allow classes for emojis
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
// Allow icons, emojis, chroma syntax and keyword markup on span
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
// Allow 'style' attribute on text elements.
policy.AllowAttrs("style").OnElements("span", "p")
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
policy.AllowStyles("color", "background-color").OnElements("span", "p")
@ -144,7 +137,7 @@ func createDefaultPolicy() *bluemonday.Policy {
generalSafeElements := []string{
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote",
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
"details", "caption", "figure", "figcaption",
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",

View File

@ -4,6 +4,7 @@
package admin
import (
"fmt"
"net/http"
"net/url"
"strings"
@ -84,7 +85,7 @@ func UnadoptedRepos(ctx *context.Context) {
if !doSearch {
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "search", "search")
pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
return
@ -98,7 +99,7 @@ func UnadoptedRepos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "search", "search")
pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
}

View File

@ -6,6 +6,7 @@ package explore
import (
"net/http"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
@ -76,7 +77,16 @@ func Code(ctx *context.Context) {
)
if (len(repoIDs) > 0) || isAdmin {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
RepoIDs: repoIDs,
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
Language: language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
},
})
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@ -127,7 +137,7 @@ func Code(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplExploreCode)

View File

@ -169,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
pager := context.NewPagination(int(count), opts.PageSize, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "topic", "TopicOnly")
pager.AddParam(ctx, "language", "Language")
pager.AddParamString("topic", fmt.Sprint(topicOnly))
pager.AddParamString("language", language)
pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
ctx.Data["Page"] = pager

View File

@ -154,7 +154,7 @@ func Home(ctx *context.Context) {
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "language", "Language")
pager.AddParamString("language", language)
ctx.Data["Page"] = pager
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0

View File

@ -120,7 +120,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
pager.AddParam(ctx, "state", "State")
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)

View File

@ -163,8 +163,8 @@ func Graph(ctx *context.Context) {
ctx.Data["CommitCount"] = commitsCount
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParam(ctx, "mode", "Mode")
paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
paginator.AddParamString("mode", mode)
paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs))
for _, branch := range branches {
paginator.AddParamString("branch", branch)
}

View File

@ -472,16 +472,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
}
ctx.Data["ShowArchivedLabels"] = archived
pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "type", "ViewType")
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
pager.AddParam(ctx, "labels", "SelectLabels")
pager.AddParam(ctx, "milestone", "MilestoneID")
pager.AddParam(ctx, "project", "ProjectID")
pager.AddParam(ctx, "assignee", "AssigneeID")
pager.AddParam(ctx, "poster", "PosterID")
pager.AddParam(ctx, "archived", "ShowArchivedLabels")
pager.AddParamString("q", keyword)
pager.AddParamString("type", viewType)
pager.AddParamString("sort", sortType)
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
pager.AddParamString("labels", fmt.Sprint(selectLabels))
pager.AddParamString("milestone", fmt.Sprint(milestoneID))
pager.AddParamString("project", fmt.Sprint(projectID))
pager.AddParamString("assignee", fmt.Sprint(assigneeID))
pager.AddParamString("poster", fmt.Sprint(posterID))
pager.AddParamString("archived", fmt.Sprint(archived))
ctx.Data["Page"] = pager
}

View File

@ -106,8 +106,8 @@ func Milestones(ctx *context.Context) {
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "state", "State")
pager.AddParam(ctx, "q", "Keyword")
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
pager.AddParamString("q", keyword)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestone)

View File

@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParam(ctx, "q", "Query")
pager.AddParam(ctx, "type", "PackageType")
pager.AddParamString("q", query)
pager.AddParamString("type", packageType)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplPackagesList)

View File

@ -118,7 +118,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
pager.AddParam(ctx, "state", "State")
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)

View File

@ -6,6 +6,7 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
@ -41,8 +42,16 @@ func Search(ctx *context.Context) {
page = 1
}
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
RepoIDs: []int64{ctx.Repo.Repository.ID},
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
Language: language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
},
})
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@ -59,7 +68,7 @@ func Search(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSearch)

View File

@ -6,6 +6,7 @@ package user
import (
"net/http"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
@ -74,7 +75,16 @@ func CodeSearch(ctx *context.Context) {
)
if len(repoIDs) > 0 {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isFuzzy)
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
RepoIDs: repoIDs,
Keyword: keyword,
IsKeywordFuzzy: isFuzzy,
Language: language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
},
})
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@ -112,7 +122,7 @@ func CodeSearch(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUserCode)

View File

@ -133,7 +133,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Feeds"] = feeds
pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
pager.AddParam(ctx, "date", "Date")
pager.AddParamString("date", date)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplDashboard)
@ -329,10 +329,10 @@ func Milestones(ctx *context.Context) {
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "repos", "RepoIDs")
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
pager.AddParamString("q", keyword)
pager.AddParamString("repos", reposQuery)
pager.AddParamString("sort", sortType)
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestones)
@ -632,13 +632,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
pager.AddParam(ctx, "q", "Keyword")
pager.AddParam(ctx, "type", "ViewType")
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
pager.AddParam(ctx, "labels", "SelectLabels")
pager.AddParam(ctx, "milestone", "MilestoneID")
pager.AddParam(ctx, "assignee", "AssigneeID")
pager.AddParamString("q", keyword)
pager.AddParamString("type", viewType)
pager.AddParamString("sort", sortType)
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
pager.AddParamString("labels", selectedLabels)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues)

View File

@ -344,8 +344,8 @@ func NotificationSubscriptions(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
return
}
pager.AddParam(ctx, "sort", "SortType")
pager.AddParam(ctx, "state", "State")
pager.AddParamString("sort", sortType)
pager.AddParamString("state", state)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplNotificationSubscriptions)

View File

@ -125,8 +125,8 @@ func ListPackages(ctx *context.Context) {
}
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParam(ctx, "q", "Query")
pager.AddParam(ctx, "type", "PackageType")
pager.AddParamString("q", query)
pager.AddParamString("type", packageType)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplPackagesList)

View File

@ -324,12 +324,14 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
pager := context.NewPagination(total, pagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "tab", "TabName")
pager.AddParamString("tab", tab)
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
pager.AddParam(ctx, "language", "Language")
pager.AddParamString("language", language)
}
if tab == "activity" {
pager.AddParam(ctx, "date", "Date")
if ctx.Data["Date"] != nil {
pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
}
}
ctx.Data["Page"] = pager
}

View File

@ -26,17 +26,6 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination {
return p
}
// AddParam adds a value from context identified by ctxKey as link param under a given paramKey
func (p *Pagination) AddParam(ctx *Context, paramKey, ctxKey string) {
_, exists := ctx.Data[ctxKey]
if !exists {
return
}
paramData := fmt.Sprintf("%v", ctx.Data[ctxKey]) // cast any to string
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(paramKey), url.QueryEscape(paramData))
p.urlParams = append(p.urlParams, urlParam)
}
// AddParamString adds a string parameter directly
func (p *Pagination) AddParamString(key, value string) {
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
@ -50,8 +39,14 @@ func (p *Pagination) GetParams() template.URL {
// SetDefaultParams sets common pagination params that are often used
func (p *Pagination) SetDefaultParams(ctx *Context) {
p.AddParam(ctx, "sort", "SortType")
p.AddParam(ctx, "q", "Keyword")
if v, ok := ctx.Data["SortType"].(string); ok {
p.AddParamString("sort", v)
}
if v, ok := ctx.Data["Keyword"].(string); ok {
p.AddParamString("q", v)
}
if v, ok := ctx.Data["IsFuzzy"].(bool); ok {
p.AddParamString("fuzzy", fmt.Sprint(v))
}
// do not add any more uncommon params here!
p.AddParam(ctx, "fuzzy", "IsFuzzy")
}

View File

@ -119,7 +119,7 @@ func TestPullView_CodeOwner(t *testing.T) {
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{
BaseRepo: repo,
Name: "test_codeowner_fork",
Name: "test_codeowner",
})
assert.NoError(t, err)
@ -137,9 +137,9 @@ func TestPullView_CodeOwner(t *testing.T) {
assert.NoError(t, err)
session := loginUser(t, "user5")
testPullCreate(t, session, "user5", "test_codeowner_fork", false, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
testPullCreate(t, session, "user5", "test_codeowner", true, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2")
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch-forked"})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"})
unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
})
})

View File

@ -149,24 +149,32 @@ a {
text-decoration-skip-ink: all;
}
/* muted link = only colored when hovered */
/* silenced link = never colored */
/* a = always colored, underlined on hover */
/* a.muted = colored on hover, underlined on hover */
/* a.suppressed = never colored, underlined on hover */
/* a.silenced = never colored, never underlined */
a.muted,
a.suppressed,
a.silenced,
.muted-links a {
color: inherit;
}
a:hover,
a.suppressed:hover,
a.muted:hover,
a.muted:hover [class*="color-text"],
.muted-links a:hover {
color: var(--color-primary);
}
a.silenced:hover {
a.silenced:hover,
a.suppressed:hover {
color: inherit;
}
a.silenced:hover {
text-decoration: none;
}
@ -1279,42 +1287,47 @@ input:-webkit-autofill:active,
border-radius: var(--border-radius);
}
.attention {
.attention-header {
padding: 0.5em 0.75em !important;
color: var(--color-text) !important;
}
.attention-icon {
margin: 2px 6px 0 0;
}
blockquote.attention-note {
border-left-color: var(--color-blue-dark-1);
}
strong.attention-note, span.attention-note {
strong.attention-note, svg.attention-note {
color: var(--color-blue-dark-1);
}
blockquote.attention-tip {
border-left-color: var(--color-success-text);
}
strong.attention-tip, span.attention-tip {
strong.attention-tip, svg.attention-tip {
color: var(--color-success-text);
}
blockquote.attention-important {
border-left-color: var(--color-violet-dark-1);
}
strong.attention-important, span.attention-important {
strong.attention-important, svg.attention-important {
color: var(--color-violet-dark-1);
}
blockquote.attention-warning {
border-left-color: var(--color-warning-text);
}
strong.attention-warning, span.attention-warning {
strong.attention-warning, svg.attention-warning {
color: var(--color-warning-text);
}
blockquote.attention-caution {
border-left-color: var(--color-red-dark-1);
}
strong.attention-caution, span.attention-caution {
strong.attention-caution, svg.attention-caution {
color: var(--color-red-dark-1);
}

View File

@ -30,33 +30,33 @@
--color-primary-alpha-90: #4183c4e1;
--color-primary-hover: var(--color-primary-dark-1);
--color-primary-active: var(--color-primary-dark-2);
--color-secondary: #dedede;
--color-secondary-dark-1: #cecece;
--color-secondary-dark-2: #bfbfbf;
--color-secondary-dark-3: #a0a0a0;
--color-secondary-dark-4: #909090;
--color-secondary-dark-5: #818181;
--color-secondary-dark-6: #717171;
--color-secondary-dark-7: #626262;
--color-secondary-dark-8: #525252;
--color-secondary-dark-9: #434343;
--color-secondary-dark-10: #333333;
--color-secondary-dark-11: #242424;
--color-secondary-dark-12: #141414;
--color-secondary-dark-13: #040404;
--color-secondary-light-1: #e5e5e5;
--color-secondary-light-2: #ebebeb;
--color-secondary-light-3: #f2f2f2;
--color-secondary-light-4: #f8f8f8;
--color-secondary-alpha-10: #dedede19;
--color-secondary-alpha-20: #dedede33;
--color-secondary-alpha-30: #dedede4b;
--color-secondary-alpha-40: #dedede66;
--color-secondary-alpha-50: #dedede80;
--color-secondary-alpha-60: #dedede99;
--color-secondary-alpha-70: #dededeb3;
--color-secondary-alpha-80: #dededecc;
--color-secondary-alpha-90: #dededee1;
--color-secondary: #d0d7de;
--color-secondary-dark-1: #c7ced5;
--color-secondary-dark-2: #b9c0c7;
--color-secondary-dark-3: #99a0a7;
--color-secondary-dark-4: #899097;
--color-secondary-dark-5: #7a8188;
--color-secondary-dark-6: #6a7178;
--color-secondary-dark-7: #5b6269;
--color-secondary-dark-8: #4b5259;
--color-secondary-dark-9: #3c434a;
--color-secondary-dark-10: #2c333a;
--color-secondary-dark-11: #1d242b;
--color-secondary-dark-12: #0d141b;
--color-secondary-dark-13: #00040b;
--color-secondary-light-1: #dee5ec;
--color-secondary-light-2: #e4ebf2;
--color-secondary-light-3: #ebf2f9;
--color-secondary-light-4: #f1f8ff;
--color-secondary-alpha-10: #d0d7de19;
--color-secondary-alpha-20: #d0d7de33;
--color-secondary-alpha-30: #d0d7de4b;
--color-secondary-alpha-40: #d0d7de66;
--color-secondary-alpha-50: #d0d7de80;
--color-secondary-alpha-60: #d0d7de99;
--color-secondary-alpha-70: #d0d7deb3;
--color-secondary-alpha-80: #d0d7decc;
--color-secondary-alpha-90: #d0d7dee1;
--color-secondary-button: var(--color-secondary-dark-4);
--color-secondary-hover: var(--color-secondary-dark-5);
--color-secondary-active: var(--color-secondary-dark-6);
@ -81,7 +81,7 @@
--color-purple: #a333c8;
--color-pink: #e03997;
--color-brown: #a5673f;
--color-black: #1b1c1d;
--color-black: #191c1d;
/* light variants - produced via Sass scale-color(color, $lightness: +25%) */
--color-red-light: #e45e5e;
--color-orange-light: #f59555;
@ -107,7 +107,7 @@
--color-purple-dark-1: #932eb4;
--color-pink-dark-1: #db228a;
--color-brown-dark-1: #955d39;
--color-black-dark-1: #18191a;
--color-black-dark-1: #16191c;
/* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */
--color-red-dark-2: #b11e1e;
--color-orange-dark-2: #cc580c;
@ -120,7 +120,7 @@
--color-purple-dark-2: #8229a0;
--color-pink-dark-2: #c21e7b;
--color-brown-dark-2: #845232;
--color-black-dark-2: #161617;
--color-black-dark-2: #131619;
/* ansi colors used for actions console and console files */
--color-ansi-black: #1f2326;
--color-ansi-red: #cc4848;
@ -139,8 +139,8 @@
--color-ansi-bright-cyan: #00b6ad;
--color-ansi-bright-white: var(--color-console-fg);
/* other colors */
--color-grey: #707070;
--color-grey-light: #838383;
--color-grey: #697077;
--color-grey-light: #7c838a;
--color-gold: #a1882b;
--color-white: #ffffff;
--color-diff-removed-word-bg: #fdb8c0;
@ -151,7 +151,7 @@
--color-diff-removed-row-border: #f1c0c0;
--color-diff-moved-row-border: #d0e27f;
--color-diff-added-row-border: #e6ffed;
--color-diff-inactive: #f2f2f2;
--color-diff-inactive: #f0f2f4;
--color-error-border: #e0b4b4;
--color-error-bg: #fff6f6;
--color-error-bg-active: #fbb;
@ -181,56 +181,56 @@
--color-git: #f05133;
/* target-based colors */
--color-body: #ffffff;
--color-box-header: #f7f7f7;
--color-box-header: #f1f3f5;
--color-box-body: #ffffff;
--color-box-body-highlight: #fafafa;
--color-text-dark: #080808;
--color-text: #212121;
--color-text-light: #555555;
--color-text-light-1: #6a6a6a;
--color-text-light-2: #808080;
--color-text-light-3: #a0a0a0;
--color-box-body-highlight: #f4faff;
--color-text-dark: #03080d;
--color-text: #1c2126;
--color-text-light: #3c434a;
--color-text-light-1: #4b5259;
--color-text-light-2: #6a7178;
--color-text-light-3: #899097;
--color-footer: var(--color-nav-bg);
--color-timeline: #ececec;
--color-timeline: #d0d7de;
--color-input-text: var(--color-text-dark);
--color-input-background: #fafafa;
--color-input-toggle-background: #dedede;
--color-input-background: #f8f9fb;
--color-input-toggle-background: #d0d7de;
--color-input-border: var(--color-secondary);
--color-input-border-hover: var(--color-secondary-dark-1);
--color-header-wrapper: transparent;
--color-light: #00000006;
--color-header-wrapper: #fafbfc;
--color-light: #00001706;
--color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
--color-light-border: #0000001d;
--color-hover: #00000014;
--color-active: #0000001b;
--color-menu: #fafafa;
--color-card: #fafafa;
--color-markup-table-row: #00000008;
--color-markup-code-block: #00000010;
--color-button: #fafafa;
--color-code-bg: #ffffff;
--color-code-sidebar-bg: #f5f5f5;
--color-shadow: #00000026;
--color-secondary-bg: #f4f4f4;
--color-light-border: #0000171d;
--color-hover: #00001708;
--color-active: #00001714;
--color-menu: #f8f9fb;
--color-card: #f8f9fb;
--color-markup-table-row: #00001708;
--color-markup-code-block: #00001710;
--color-button: #f8f9fb;
--color-code-bg: #fafdff;
--color-code-sidebar-bg: #f2f5f8;
--color-shadow: #00001726;
--color-secondary-bg: #f2f5f8;
--color-expand-button: #d8efff;
--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: #111111;
--color-project-board-light-label: #eeeeee;
--color-project-board-dark-label: #0e1114;
--color-project-board-light-label: #eaeef2;
--color-caret: var(--color-text-dark);
--color-reaction-bg: #0000000a;
--color-reaction-bg: #0000170a;
--color-reaction-hover-bg: var(--color-primary-light-5);
--color-reaction-active-bg: var(--color-primary-light-6);
--color-tooltip-text: #ffffff;
--color-tooltip-bg: #000000f0;
--color-nav-bg: #ffffff;
--color-tooltip-text: #fbfdff;
--color-tooltip-bg: #000017f0;
--color-nav-bg: #f8f9fb;
--color-nav-hover-bg: var(--color-secondary-light-1);
--color-nav-text: var(--color-text);
--color-label-text: var(--color-text);
--color-label-bg: #9d9d9d4b;
--color-label-hover-bg: #9d9d9da0;
--color-label-active-bg: #9d9d9dff;
--color-label-bg: #949da64b;
--color-label-hover-bg: #949da6a0;
--color-label-active-bg: #949da6ff;
--color-accent: var(--color-primary-light-1);
--color-small-accent: var(--color-primary-light-6);
--color-active-line: #fffbdd;

View File

@ -457,7 +457,7 @@ export function initRepositoryActionView() {
</div>
</div>
</div>
<div class="job-step-container" ref="steps">
<div class="job-step-container" ref="steps" v-if="currentJob.steps.length">
<div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
<div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''">
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
@ -686,11 +686,15 @@ export function initRepositoryActionView() {
background-color: var(--color-console-bg);
position: sticky;
top: 0;
border-radius: var(--border-radius) var(--border-radius) 0 0;
border-radius: var(--border-radius);
height: 60px;
z-index: 1;
}
.job-info-header:has(+ .job-step-container) {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
.job-info-header .job-info-header-title {
color: var(--color-console-fg);
font-size: 16px;

View File

@ -123,7 +123,7 @@ const sfc = {
return -1;
},
scrollToActive() {
let el = this.$refs[`listItem${this.active}`];
let el = this.$refs[`listItem${this.active}`]; // eslint-disable-line no-jquery/variable-pattern
if (!el || !el.length) return;
if (Array.isArray(el)) {
el = el[0];

View File

@ -49,7 +49,7 @@ export function initAdminCommon() {
}
function onUsePagedSearchChange() {
if ($('#use_paged_search').prop('checked')) {
if (document.getElementById('use_paged_search').checked) {
showElem('.search-page-size');
$('.search-page-size').find('input').attr('required', 'required');
} else {

View File

@ -231,8 +231,8 @@ export function initDropzone(el) {
init() {
this.on('success', (file, data) => {
file.uuid = data.uuid;
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append(input);
const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append($input);
// Create a "Copy Link" element, to conveniently copy the image
// or file link as Markdown to the clipboard
const copyLinkElement = document.createElement('div');
@ -305,15 +305,15 @@ export function initGlobalLinkActions() {
filter += `#${$this.attr('data-modal-id')}`;
}
const dialog = $(`.delete.modal${filter}`);
dialog.find('.name').text($this.data('name'));
const $dialog = $(`.delete.modal${filter}`);
$dialog.find('.name').text($this.data('name'));
for (const [key, value] of Object.entries(dataArray)) {
if (key && key.startsWith('data')) {
dialog.find(`.${key}`).text(value);
$dialog.find(`.${key}`).text(value);
}
}
dialog.modal({
$dialog.modal({
closable: false,
onApprove: async () => {
if ($this.data('type') === 'form') {
@ -380,8 +380,8 @@ function initGlobalShowModal() {
$attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
}
}
const colorPickers = $modal.find('.color-picker');
if (colorPickers.length > 0) {
const $colorPickers = $modal.find('.color-picker');
if ($colorPickers.length > 0) {
initCompColorPicker(); // FIXME: this might cause duplicate init
}
$modal.modal('setting', {

View File

@ -6,23 +6,23 @@ function isExclusiveScopeName(name) {
}
function updateExclusiveLabelEdit(form) {
const nameInput = $(`${form} .label-name-input`);
const exclusiveField = $(`${form} .label-exclusive-input-field`);
const exclusiveCheckbox = $(`${form} .label-exclusive-input`);
const exclusiveWarning = $(`${form} .label-exclusive-warning`);
const $nameInput = $(`${form} .label-name-input`);
const $exclusiveField = $(`${form} .label-exclusive-input-field`);
const $exclusiveCheckbox = $(`${form} .label-exclusive-input`);
const $exclusiveWarning = $(`${form} .label-exclusive-warning`);
if (isExclusiveScopeName(nameInput.val())) {
exclusiveField.removeClass('muted');
exclusiveField.removeAttr('aria-disabled');
if (exclusiveCheckbox.prop('checked') && exclusiveCheckbox.data('exclusive-warn')) {
exclusiveWarning.removeClass('gt-hidden');
if (isExclusiveScopeName($nameInput.val())) {
$exclusiveField.removeClass('muted');
$exclusiveField.removeAttr('aria-disabled');
if ($exclusiveCheckbox[0].checked && $exclusiveCheckbox.data('exclusive-warn')) {
$exclusiveWarning.removeClass('gt-hidden');
} else {
exclusiveWarning.addClass('gt-hidden');
$exclusiveWarning.addClass('gt-hidden');
}
} else {
exclusiveField.addClass('muted');
exclusiveField.attr('aria-disabled', 'true');
exclusiveWarning.addClass('gt-hidden');
$exclusiveField.addClass('muted');
$exclusiveField.attr('aria-disabled', 'true');
$exclusiveWarning.addClass('gt-hidden');
}
}
@ -46,18 +46,18 @@ export function initCompLabelEdit(selector) {
$('.edit-label .color-picker').minicolors('value', $(this).data('color'));
$('#label-modal-id').val($(this).data('id'));
const nameInput = $('.edit-label .label-name-input');
nameInput.val($(this).data('title'));
const $nameInput = $('.edit-label .label-name-input');
$nameInput.val($(this).data('title'));
const isArchivedCheckbox = $('.edit-label .label-is-archived-input');
isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived'));
const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
$isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
const exclusiveCheckbox = $('.edit-label .label-exclusive-input');
exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive'));
const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
$exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
// Warn when label was previously not exclusive and used in issues
exclusiveCheckbox.data('exclusive-warn',
$exclusiveCheckbox.data('exclusive-warn',
$(this).data('num-issues') > 0 &&
(!this.hasAttribute('data-exclusive') || !isExclusiveScopeName(nameInput.val())));
(!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
updateExclusiveLabelEdit('.edit-label');
$('.edit-label .label-desc-input').val($(this).data('description'));

View File

@ -17,21 +17,21 @@ export function initCompReactionSelector($parent) {
const data = await res.json();
if (data && (data.html || data.empty)) {
const content = $(this).closest('.content');
let react = content.find('.segment.reactions');
if ((!data.empty || data.html === '') && react.length > 0) {
react.remove();
const $content = $(this).closest('.content');
let $react = $content.find('.segment.reactions');
if ((!data.empty || data.html === '') && $react.length > 0) {
$react.remove();
}
if (!data.empty) {
const attachments = content.find('.segment.bottom:first');
react = $(data.html);
if (attachments.length > 0) {
react.insertBefore(attachments);
const $attachments = $content.find('.segment.bottom:first');
$react = $(data.html);
if ($attachments.length > 0) {
$react.insertBefore($attachments);
} else {
react.appendTo(content);
$react.appendTo($content);
}
react.find('.dropdown').dropdown();
initCompReactionSelector(react);
$react.find('.dropdown').dropdown();
initCompReactionSelector($react);
}
}
});

View File

@ -1,6 +1,7 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.js';
const {appSubUrl, csrfToken, notificationSettings, assetVersionEncoded} = window.config;
const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
let notificationSequenceNumber = 0;
export function initNotificationsTable() {
@ -27,25 +28,6 @@ export function initNotificationsTable() {
e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
});
}
$('#notification_table .button').on('click', function () {
(async () => {
const data = await updateNotification(
$(this).data('url'),
$(this).data('status'),
$(this).data('page'),
$(this).data('q'),
$(this).data('notification-id'),
);
if ($(data).data('sequence-number') === notificationSequenceNumber) {
$('#notification_div').replaceWith(data);
initNotificationsTable();
}
await updateNotificationCount();
})();
return false;
});
}
async function receiveUpdateCount(event) {
@ -63,9 +45,9 @@ async function receiveUpdateCount(event) {
}
export function initNotificationCount() {
const notificationCount = $('.notification_count');
const $notificationCount = $('.notification_count');
if (!notificationCount.length) {
if (!$notificationCount.length) {
return;
}
@ -73,7 +55,7 @@ export function initNotificationCount() {
const startPeriodicPoller = (timeout, lastCount) => {
if (timeout <= 0 || !Number.isFinite(timeout)) return;
usingPeriodicPoller = true;
lastCount = lastCount ?? notificationCount.text();
lastCount = lastCount ?? $notificationCount.text();
setTimeout(async () => {
await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
}, timeout);
@ -161,60 +143,52 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
}
async function updateNotificationTable() {
const notificationDiv = $('#notification_div');
if (notificationDiv.length > 0) {
const data = await $.ajax({
type: 'GET',
url: `${appSubUrl}/notifications${window.location.search}`,
data: {
'div-only': true,
'sequence-number': ++notificationSequenceNumber,
const notificationDiv = document.getElementById('notification_div');
if (notificationDiv) {
try {
const params = new URLSearchParams(window.location.search);
params.set('div-only', true);
params.set('sequence-number', ++notificationSequenceNumber);
const url = `${appSubUrl}/notifications?${params.toString()}`;
const response = await GET(url);
if (!response.ok) {
throw new Error('Failed to fetch notification table');
}
});
if ($(data).data('sequence-number') === notificationSequenceNumber) {
notificationDiv.replaceWith(data);
initNotificationsTable();
const data = await response.text();
if ($(data).data('sequence-number') === notificationSequenceNumber) {
notificationDiv.outerHTML = data;
initNotificationsTable();
}
} catch (error) {
console.error(error);
}
}
}
async function updateNotificationCount() {
const data = await $.ajax({
type: 'GET',
url: `${appSubUrl}/notifications/new`,
headers: {
'X-Csrf-Token': csrfToken,
},
});
try {
const response = await GET(`${appSubUrl}/notifications/new`);
const notificationCount = $('.notification_count');
if (data.new === 0) {
notificationCount.addClass('gt-hidden');
} else {
notificationCount.removeClass('gt-hidden');
if (!response.ok) {
throw new Error('Failed to fetch notification count');
}
const data = await response.json();
const $notificationCount = $('.notification_count');
if (data.new === 0) {
$notificationCount.addClass('gt-hidden');
} else {
$notificationCount.removeClass('gt-hidden');
}
$notificationCount.text(`${data.new}`);
return `${data.new}`;
} catch (error) {
console.error(error);
return '0';
}
notificationCount.text(`${data.new}`);
return `${data.new}`;
}
async function updateNotification(url, status, page, q, notificationID) {
if (status !== 'pinned') {
$(`#notification_${notificationID}`).remove();
}
return $.ajax({
type: 'POST',
url,
data: {
_csrf: csrfToken,
notification_id: notificationID,
status,
page,
q,
noredirect: true,
'sequence-number': ++notificationSequenceNumber,
},
});
}

View File

@ -97,10 +97,10 @@ function initRepoDiffConversationForm() {
const data = await response.text();
if ($(this).closest('.conversation-holder').length) {
const conversation = $(data);
$(this).closest('.conversation-holder').replaceWith(conversation);
conversation.find('.dropdown').dropdown();
initCompReactionSelector(conversation);
const $conversation = $(data);
$(this).closest('.conversation-holder').replaceWith($conversation);
$conversation.find('.dropdown').dropdown();
initCompReactionSelector($conversation);
} else {
window.location.reload();
}
@ -209,8 +209,8 @@ function initRepoDiffShowMore() {
export function initRepoDiffView() {
initRepoDiffConversationForm();
const diffFileList = $('#diff-file-list');
if (diffFileList.length === 0) return;
const $diffFileList = $('#diff-file-list');
if ($diffFileList.length === 0) return;
initDiffFileTree();
initDiffCommitSelect();
initRepoDiffShowMore();

View File

@ -15,9 +15,9 @@ function initEditPreviewTab($form) {
const $this = $(this);
let context = `${$this.data('context')}/`;
const mode = $this.data('markup-mode') || 'comment';
const treePathEl = $form.find('input#tree_path');
if (treePathEl.length > 0) {
context += treePathEl.val();
const $treePathEl = $form.find('input#tree_path');
if ($treePathEl.length > 0) {
context += $treePathEl.val();
}
context = context.substring(0, context.lastIndexOf('/'));
@ -25,7 +25,7 @@ function initEditPreviewTab($form) {
formData.append('mode', mode);
formData.append('context', context);
formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val());
formData.append('file_path', treePathEl.val());
formData.append('file_path', $treePathEl.val());
try {
const response = await POST($this.data('url'), {data: formData});
const data = await response.text();
@ -67,10 +67,10 @@ export function initRepoEditor() {
$('.js-quick-pull-choice-option').on('change', function () {
if ($(this).val() === 'commit-to-new-branch') {
showElem($('.quick-pull-branch-name'));
$('.quick-pull-branch-name input').prop('required', true);
document.querySelector('.quick-pull-branch-name input').required = true;
} else {
hideElem($('.quick-pull-branch-name'));
$('.quick-pull-branch-name input').prop('required', false);
document.querySelector('.quick-pull-branch-name input').required = false;
}
$('#commit-button').text($(this).attr('button_text'));
});
@ -78,11 +78,11 @@ export function initRepoEditor() {
const joinTreePath = ($fileNameEl) => {
const parts = [];
$('.breadcrumb span.section').each(function () {
const element = $(this);
if (element.find('a').length) {
parts.push(element.find('a').text());
const $element = $(this);
if ($element.find('a').length) {
parts.push($element.find('a').text());
} else {
parts.push(element.text());
parts.push($element.text());
}
});
if ($fileNameEl.val()) parts.push($fileNameEl.val());
@ -135,13 +135,13 @@ export function initRepoEditor() {
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
const $commitButton = $('#commit-button');
const commitButton = document.getElementById('commit-button');
const $editForm = $('.ui.edit.form');
const dirtyFileClass = 'dirty-file';
// Disabling the button at the start
if ($('input[name="page_has_posted"]').val() !== 'true') {
$commitButton.prop('disabled', true);
commitButton.disabled = true;
}
// Registering a custom listener for the file path and the file content
@ -151,7 +151,7 @@ export function initRepoEditor() {
fieldSelector: ':input:not(.commit-form-wrapper :input)',
change() {
const dirty = $(this).hasClass(dirtyFileClass);
$commitButton.prop('disabled', !dirty);
commitButton.disabled = !dirty;
},
});
@ -163,7 +163,7 @@ export function initRepoEditor() {
editor.setValue(value);
}
$commitButton.on('click', (event) => {
commitButton?.addEventListener('click', (e) => {
// A modal which asks if an empty file should be committed
if ($editArea.val().length === 0) {
$('#edit-empty-content-modal').modal({
@ -171,7 +171,7 @@ export function initRepoEditor() {
$('.edit.form').trigger('submit');
},
}).modal('show');
event.preventDefault();
e.preventDefault();
}
});
})();
@ -181,6 +181,6 @@ export function renderPreviewPanelContent($panelPreviewer, data) {
$panelPreviewer.html(data);
initMarkupContent();
const refIssues = $panelPreviewer.find('p .ref-issue');
attachRefIssueContextPopup(refIssues);
const $refIssues = $panelPreviewer.find('p .ref-issue');
attachRefIssueContextPopup($refIssues);
}

View File

@ -63,10 +63,10 @@ export function initRepoGraphGit() {
(async () => {
const response = await GET(String(ajaxUrl));
const html = await response.text();
const div = $(html);
$('#pagination').html(div.find('#pagination').html());
$('#rel-container').html(div.find('#rel-container').html());
$('#rev-container').html(div.find('#rev-container').html());
const $div = $(html);
$('#pagination').html($div.find('#pagination').html());
$('#rel-container').html($div.find('#rel-container').html());
$('#rev-container').html($div.find('#rev-container').html());
$('#loading-indicator').addClass('gt-hidden');
$('#rel-container').removeClass('gt-hidden');
$('#rev-container').removeClass('gt-hidden');

View File

@ -6,55 +6,55 @@ import {POST} from '../modules/fetch.js';
const {appSubUrl} = window.config;
export function initRepoTopicBar() {
const mgrBtn = $('#manage_topic');
if (!mgrBtn.length) return;
const editDiv = $('#topic_edit');
const viewDiv = $('#repo-topics');
const saveBtn = $('#save_topic');
const topicDropdown = $('#topic_edit .dropdown');
const topicForm = editDiv; // the old logic, editDiv is topicForm
const topicDropdownSearch = topicDropdown.find('input.search');
const $mgrBtn = $('#manage_topic');
if (!$mgrBtn.length) return;
const $editDiv = $('#topic_edit');
const $viewDiv = $('#repo-topics');
const $saveBtn = $('#save_topic');
const $topicDropdown = $('#topic_edit .dropdown');
const $topicForm = $editDiv; // the old logic, $editDiv is topicForm
const $topicDropdownSearch = $topicDropdown.find('input.search');
const topicPrompts = {
countPrompt: topicDropdown.attr('data-text-count-prompt'),
formatPrompt: topicDropdown.attr('data-text-format-prompt'),
countPrompt: $topicDropdown.attr('data-text-count-prompt'),
formatPrompt: $topicDropdown.attr('data-text-format-prompt'),
};
mgrBtn.on('click', () => {
hideElem(viewDiv);
showElem(editDiv);
topicDropdownSearch.focus();
$mgrBtn.on('click', () => {
hideElem($viewDiv);
showElem($editDiv);
$topicDropdownSearch.trigger('focus');
});
$('#cancel_topic_edit').on('click', () => {
hideElem(editDiv);
showElem(viewDiv);
mgrBtn.focus();
hideElem($editDiv);
showElem($viewDiv);
$mgrBtn.trigger('focus');
});
saveBtn.on('click', async () => {
$saveBtn.on('click', async () => {
const topics = $('input[name=topics]').val();
const data = new FormData();
data.append('topics', topics);
const response = await POST(saveBtn.attr('data-link'), {data});
const response = await POST($saveBtn.attr('data-link'), {data});
if (response.ok) {
const responseData = await response.json();
if (responseData.status === 'ok') {
viewDiv.children('.topic').remove();
$viewDiv.children('.topic').remove();
if (topics.length) {
const topicArray = topics.split(',');
topicArray.sort();
for (const topic of topicArray) {
const link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
link.text(topic);
link.insertBefore(mgrBtn); // insert all new topics before manage button
const $link = $('<a class="ui repo-topic large label topic gt-m-0"></a>');
$link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`);
$link.text(topic);
$link.insertBefore($mgrBtn); // insert all new topics before manage button
}
}
hideElem(editDiv);
showElem(viewDiv);
hideElem($editDiv);
showElem($viewDiv);
}
} else if (response.status === 422) {
const responseData = await response.json();
@ -62,10 +62,10 @@ export function initRepoTopicBar() {
topicPrompts.formatPrompt = responseData.message;
const {invalidTopics} = responseData;
const topicLabels = topicDropdown.children('a.ui.label');
const $topicLabels = $topicDropdown.children('a.ui.label');
for (const [index, value] of topics.split(',').entries()) {
if (invalidTopics.includes(value)) {
topicLabels.eq(index).removeClass('green').addClass('red');
$topicLabels.eq(index).removeClass('green').addClass('red');
}
}
} else {
@ -74,10 +74,10 @@ export function initRepoTopicBar() {
}
// Always validate the form
topicForm.form('validate form');
$topicForm.form('validate form');
});
topicDropdown.dropdown({
$topicDropdown.dropdown({
allowAdditions: true,
forceSelection: false,
fullTextSearch: 'exact',
@ -100,7 +100,7 @@ export function initRepoTopicBar() {
const query = stripTags(this.urlData.query.trim());
let found_query = false;
const current_topics = [];
topicDropdown.find('a.label.visible').each((_, el) => {
$topicDropdown.find('a.label.visible').each((_, el) => {
current_topics.push(el.getAttribute('data-value'));
});
@ -150,15 +150,15 @@ export function initRepoTopicBar() {
});
$.fn.form.settings.rules.validateTopic = function (_values, regExp) {
const topics = topicDropdown.children('a.ui.label');
const status = topics.length === 0 || topics.last().attr('data-value').match(regExp);
const $topics = $topicDropdown.children('a.ui.label');
const status = $topics.length === 0 || $topics.last().attr('data-value').match(regExp);
if (!status) {
topics.last().removeClass('green').addClass('red');
$topics.last().removeClass('green').addClass('red');
}
return status && topicDropdown.children('a.ui.label.red').length === 0;
return status && $topicDropdown.children('a.ui.label.red').length === 0;
};
topicForm.form({
$topicForm.form({
on: 'change',
inline: true,
fields: {

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import {updateIssuesMeta} from './repo-issue.js';
import {toggleElem, hideElem} from '../utils/dom.js';
import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js';
@ -8,32 +8,42 @@ import {createSortable} from '../modules/sortable.js';
import {DELETE, POST} from '../modules/fetch.js';
function initRepoIssueListCheckboxes() {
const $issueSelectAll = $('.issue-checkbox-all');
const $issueCheckboxes = $('.issue-checkbox');
const issueSelectAll = document.querySelector('.issue-checkbox-all');
const issueCheckboxes = document.querySelectorAll('.issue-checkbox');
const syncIssueSelectionState = () => {
const $checked = $issueCheckboxes.filter(':checked');
const anyChecked = $checked.length !== 0;
const allChecked = anyChecked && $checked.length === $issueCheckboxes.length;
const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
const anyChecked = Boolean(checkedCheckboxes.length);
const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
if (allChecked) {
$issueSelectAll.prop({'checked': true, 'indeterminate': false});
issueSelectAll.checked = true;
issueSelectAll.indeterminate = false;
} else if (anyChecked) {
$issueSelectAll.prop({'checked': false, 'indeterminate': true});
issueSelectAll.checked = false;
issueSelectAll.indeterminate = true;
} else {
$issueSelectAll.prop({'checked': false, 'indeterminate': false});
issueSelectAll.checked = false;
issueSelectAll.indeterminate = false;
}
// if any issue is selected, show the action panel, otherwise show the filter panel
toggleElem($('#issue-filters'), !anyChecked);
toggleElem($('#issue-actions'), anyChecked);
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
$('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll);
const panels = document.querySelectorAll('#issue-filters, #issue-actions');
const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
toolbarLeft.prepend(issueSelectAll);
};
$issueCheckboxes.on('change', syncIssueSelectionState);
for (const el of issueCheckboxes) {
el.addEventListener('change', syncIssueSelectionState);
}
$issueSelectAll.on('change', () => {
$issueCheckboxes.prop('checked', $issueSelectAll.is(':checked'));
issueSelectAll.addEventListener('change', () => {
for (const el of issueCheckboxes) {
el.checked = issueSelectAll.checked;
}
syncIssueSelectionState();
});
@ -125,7 +135,9 @@ function initRepoIssueListAuthorDropdown() {
if (newMenuHtml) {
const $newMenuItems = $(newMenuHtml);
$newMenuItems.addClass('dynamic-item');
$menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems);
const div = document.createElement('div');
div.classList.add('divider', 'dynamic-item');
$menu[0].append(div, ...$newMenuItems);
}
$searchDropdown.dropdown('refresh');
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function

View File

@ -144,9 +144,9 @@ export function initRepoIssueSidebarList() {
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
if (e.altKey && e.keyCode === 13) {
const selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
if (selectedItems.length > 0) {
excludeLabel($(selectedItems[0]));
const $selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected');
if ($selectedItems.length > 0) {
excludeLabel($($selectedItems[0]));
}
}
});
@ -214,12 +214,12 @@ export function initRepoIssueDependencyDelete() {
export function initRepoIssueCodeCommentCancel() {
// Cancel inline code comment
$(document).on('click', '.cancel-code-comment', (e) => {
const form = $(e.currentTarget).closest('form');
if (form.length > 0 && form.hasClass('comment-form')) {
form.addClass('gt-hidden');
showElem(form.closest('.comment-code-cloud').find('button.comment-form-reply'));
const $form = $(e.currentTarget).closest('form');
if ($form.length > 0 && $form.hasClass('comment-form')) {
$form.addClass('gt-hidden');
showElem($form.closest('.comment-code-cloud').find('button.comment-form-reply'));
} else {
form.closest('.comment-code-cloud').remove();
$form.closest('.comment-code-cloud').remove();
}
});
}
@ -370,10 +370,10 @@ export function initRepoIssueComments() {
});
$(document).on('click', (event) => {
const urlTarget = $(':target');
if (urlTarget.length === 0) return;
const $urlTarget = $(':target');
if ($urlTarget.length === 0) return;
const urlTargetId = urlTarget.attr('id');
const urlTargetId = $urlTarget.attr('id');
if (!urlTargetId) return;
if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
@ -390,18 +390,18 @@ export function initRepoIssueComments() {
export async function handleReply($el) {
hideElem($el);
const form = $el.closest('.comment-code-cloud').find('.comment-form');
form.removeClass('gt-hidden');
const $form = $el.closest('.comment-code-cloud').find('.comment-form');
$form.removeClass('gt-hidden');
const $textarea = form.find('textarea');
const $textarea = $form.find('textarea');
let editor = getComboMarkdownEditor($textarea);
if (!editor) {
// FIXME: the initialization of the dropzone is not consistent.
// When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized.
// When the form is submitted and partially reload, none of them is initialized.
const dropzone = form.find('.dropzone')[0];
const dropzone = $form.find('.dropzone')[0];
if (!dropzone.dropzone) initDropzone(dropzone);
editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor'));
editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
}
editor.focus();
return editor;
@ -413,30 +413,30 @@ export function initRepoPullRequestReview() {
if (window.history.scrollRestoration !== 'manual') {
window.history.scrollRestoration = 'manual';
}
const commentDiv = $(window.location.hash);
if (commentDiv) {
const $commentDiv = $(window.location.hash);
if ($commentDiv) {
// get the name of the parent id
const groupID = commentDiv.closest('div[id^="code-comments-"]').attr('id');
const groupID = $commentDiv.closest('div[id^="code-comments-"]').attr('id');
if (groupID && groupID.startsWith('code-comments-')) {
const id = groupID.slice(14);
const ancestorDiffBox = commentDiv.closest('.diff-file-box');
const $ancestorDiffBox = $commentDiv.closest('.diff-file-box');
// on pages like conversation, there is no diff header
const diffHeader = ancestorDiffBox.find('.diff-file-header');
const $diffHeader = $ancestorDiffBox.find('.diff-file-header');
// offset is for scrolling
let offset = 30;
if (diffHeader[0]) {
offset += $('.diff-detail-box').outerHeight() + diffHeader.outerHeight();
if ($diffHeader[0]) {
offset += $('.diff-detail-box').outerHeight() + $diffHeader.outerHeight();
}
$(`#show-outdated-${id}`).addClass('gt-hidden');
$(`#code-comments-${id}`).removeClass('gt-hidden');
$(`#code-preview-${id}`).removeClass('gt-hidden');
$(`#hide-outdated-${id}`).removeClass('gt-hidden');
// if the comment box is folded, expand it
if (ancestorDiffBox.attr('data-folded') && ancestorDiffBox.attr('data-folded') === 'true') {
setFileFolding(ancestorDiffBox[0], ancestorDiffBox.find('.fold-file')[0], false);
if ($ancestorDiffBox.attr('data-folded') && $ancestorDiffBox.attr('data-folded') === 'true') {
setFileFolding($ancestorDiffBox[0], $ancestorDiffBox.find('.fold-file')[0], false);
}
window.scrollTo({
top: commentDiv.offset().top - offset,
top: $commentDiv.offset().top - offset,
behavior: 'instant'
});
}
@ -504,12 +504,12 @@ export function initRepoPullRequestReview() {
const side = $(this).data('side');
const idx = $(this).data('idx');
const path = $(this).closest('[data-path]').data('path');
const tr = $(this).closest('tr');
const lineType = tr.data('line-type');
const $tr = $(this).closest('tr');
const lineType = $tr.data('line-type');
let ntr = tr.next();
if (!ntr.hasClass('add-comment')) {
ntr = $(`
let $ntr = $tr.next();
if (!$ntr.hasClass('add-comment')) {
$ntr = $(`
<tr class="add-comment" data-line-type="${lineType}">
${isSplit ? `
<td class="add-comment-left" colspan="4"></td>
@ -518,22 +518,22 @@ export function initRepoPullRequestReview() {
<td class="add-comment-left add-comment-right" colspan="5"></td>
`}
</tr>`);
tr.after(ntr);
$tr.after($ntr);
}
const td = ntr.find(`.add-comment-${side}`);
const commentCloud = td.find('.comment-code-cloud');
if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) {
const $td = $ntr.find(`.add-comment-${side}`);
const $commentCloud = $td.find('.comment-code-cloud');
if ($commentCloud.length === 0 && !$ntr.find('button[name="pending_review"]').length) {
try {
const response = await GET($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
const html = await response.text();
td.html(html);
td.find("input[name='line']").val(idx);
td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
td.find("input[name='path']").val(path);
$td.html(html);
$td.find("input[name='line']").val(idx);
$td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
$td.find("input[name='path']").val(path);
initDropzone(td.find('.dropzone')[0]);
const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
initDropzone($td.find('.dropzone')[0]);
const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
editor.focus();
} catch (error) {
console.error(error);
@ -646,18 +646,18 @@ export function initRepoIssueTitleEdit() {
export function initRepoIssueBranchSelect() {
const changeBranchSelect = function () {
const selectionTextField = $('#pull-target-branch');
const $selectionTextField = $('#pull-target-branch');
const baseName = selectionTextField.data('basename');
const baseName = $selectionTextField.data('basename');
const branchNameNew = $(this).data('branch');
const branchNameOld = selectionTextField.data('branch');
const branchNameOld = $selectionTextField.data('branch');
// Replace branch name to keep translation from HTML template
selectionTextField.html(selectionTextField.html().replace(
$selectionTextField.html($selectionTextField.html().replace(
`${baseName}:${branchNameOld}`,
`${baseName}:${branchNameNew}`
));
selectionTextField.data('branch', branchNameNew); // update branch name in setting
$selectionTextField.data('branch', branchNameNew); // update branch name in setting
};
$('#branch-select > .item').on('click', changeBranchSelect);
}

View File

@ -76,11 +76,11 @@ export function initRepoCommentForm() {
}
if (editMode === 'true') {
const form = $('#update_issueref_form');
const $form = $('#update_issueref_form');
const params = new URLSearchParams();
params.append('ref', selectedValue);
try {
await POST(form.attr('action'), {data: params});
await POST($form.attr('action'), {data: params});
window.location.reload();
} catch (error) {
console.error(error);
@ -139,7 +139,7 @@ export function initRepoCommentForm() {
hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var
const clickedItem = $(this);
const $clickedItem = $(this);
const scope = $(this).attr('data-scope');
$(this).parent().find('.item').each(function () {
@ -148,10 +148,10 @@ export function initRepoCommentForm() {
if ($(this).attr('data-scope') !== scope) {
return true;
}
if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) {
if (!$(this).is($clickedItem) && !$(this).hasClass('checked')) {
return true;
}
} else if (!$(this).is(clickedItem)) {
} else if (!$(this).is($clickedItem)) {
// Toggle for other labels
return true;
}
@ -352,8 +352,8 @@ async function onEditContent(event) {
this.on('success', (file, data) => {
file.uuid = data.uuid;
fileUuidDict[file.uuid] = {submitted: false};
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append(input);
const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append($input);
});
this.on('removedfile', async (file) => {
if (disableRemovedfileEvent) return;
@ -390,8 +390,8 @@ async function onEditContent(event) {
dz.files.push(attachment);
fileUuidDict[attachment.uuid] = {submitted: true};
$dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
$dropzone.find('.files').append(input);
const $input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid);
$dropzone.find('.files').append($input);
}
} catch (error) {
console.error(error);
@ -430,19 +430,18 @@ async function onEditContent(event) {
} else {
$renderContent.html(data.content);
$rawContent.text(comboMarkdownEditor.value());
const refIssues = $renderContent.find('p .ref-issue');
attachRefIssueContextPopup(refIssues);
const $refIssues = $renderContent.find('p .ref-issue');
attachRefIssueContextPopup($refIssues);
}
const $content = $segment;
if (!$content.find('.dropzone-attachments').length) {
if (data.attachments !== '') {
$content.append(`<div class="dropzone-attachments"></div>`);
$content.find('.dropzone-attachments').replaceWith(data.attachments);
$content[0].append(data.attachments);
}
} else if (data.attachments === '') {
$content.find('.dropzone-attachments').remove();
} else {
$content.find('.dropzone-attachments').replaceWith(data.attachments);
$content.find('.dropzone-attachments')[0].outerHTML = data.attachments;
}
if (dz) {
dz.emit('submit');
@ -534,7 +533,7 @@ export function initRepository() {
const gitignores = $('input[name="gitignores"]').val();
const license = $('input[name="license"]').val();
if (gitignores || license) {
$('input[name="auto_init"]').prop('checked', true);
document.querySelector('input[name="auto_init"]').checked = true;
}
});
}

View File

@ -105,14 +105,14 @@ export function initRepoProject() {
const _promise = initRepoProjectSortable();
$('.edit-project-column-modal').each(function () {
const projectHeader = $(this).closest('.project-column-header');
const projectTitleLabel = projectHeader.find('.project-column-title');
const projectTitleInput = $(this).find('.project-column-title-input');
const projectColorInput = $(this).find('#new_project_column_color');
const boardColumn = $(this).closest('.project-column');
const $projectHeader = $(this).closest('.project-column-header');
const $projectTitleLabel = $projectHeader.find('.project-column-title');
const $projectTitleInput = $(this).find('.project-column-title-input');
const $projectColorInput = $(this).find('#new_project_column_color');
const $boardColumn = $(this).closest('.project-column');
if (boardColumn.css('backgroundColor')) {
setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor')));
if ($boardColumn.css('backgroundColor')) {
setLabelColor($projectHeader, rgbToHex($boardColumn.css('backgroundColor')));
}
$(this).find('.edit-project-column-button').on('click', async function (e) {
@ -121,34 +121,34 @@ export function initRepoProject() {
try {
await PUT($(this).data('url'), {
data: {
title: projectTitleInput.val(),
color: projectColorInput.val(),
title: $projectTitleInput.val(),
color: $projectColorInput.val(),
},
});
} catch (error) {
console.error(error);
} finally {
projectTitleLabel.text(projectTitleInput.val());
projectTitleInput.closest('form').removeClass('dirty');
if (projectColorInput.val()) {
setLabelColor(projectHeader, projectColorInput.val());
$projectTitleLabel.text($projectTitleInput.val());
$projectTitleInput.closest('form').removeClass('dirty');
if ($projectColorInput.val()) {
setLabelColor($projectHeader, $projectColorInput.val());
}
boardColumn.attr('style', `background: ${projectColorInput.val()}!important`);
$boardColumn.attr('style', `background: ${$projectColorInput.val()}!important`);
$('.ui.modal').modal('hide');
}
});
});
$('.default-project-column-modal').each(function () {
const boardColumn = $(this).closest('.project-column');
const showButton = $(boardColumn).find('.default-project-column-show');
const commitButton = $(this).find('.actions > .ok.button');
const $boardColumn = $(this).closest('.project-column');
const $showButton = $($boardColumn).find('.default-project-column-show');
const $commitButton = $(this).find('.actions > .ok.button');
$(commitButton).on('click', async (e) => {
$($commitButton).on('click', async (e) => {
e.preventDefault();
try {
await POST($(showButton).data('url'));
await POST($($showButton).data('url'));
} catch (error) {
console.error(error);
} finally {
@ -158,11 +158,11 @@ export function initRepoProject() {
});
$('.show-delete-project-column-modal').each(function () {
const deleteColumnModal = $(`${$(this).attr('data-modal')}`);
const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button');
const $deleteColumnModal = $(`${$(this).attr('data-modal')}`);
const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
const deleteUrl = $(this).attr('data-url');
deleteColumnButton.on('click', async (e) => {
$deleteColumnButton.on('click', async (e) => {
e.preventDefault();
try {
@ -177,13 +177,13 @@ export function initRepoProject() {
$('#new_project_column_submit').on('click', (e) => {
e.preventDefault();
const columnTitle = $('#new_project_column');
const projectColorInput = $('#new_project_column_color_picker');
if (!columnTitle.val()) {
const $columnTitle = $('#new_project_column');
const $projectColorInput = $('#new_project_column_color_picker');
if (!$columnTitle.val()) {
return;
}
const url = e.target.getAttribute('data-url');
createNewColumn(url, columnTitle, projectColorInput);
createNewColumn(url, $columnTitle, $projectColorInput);
});
}

View File

@ -1,3 +1,3 @@
import $ from 'jquery';
window.$ = window.jQuery = $;
window.$ = window.jQuery = $; // eslint-disable-line no-jquery/variable-pattern

View File

@ -72,7 +72,9 @@ function delegateOne($dropdown) {
dropdownTemplates.menu = function(response, fields, preserveHTML, className) {
// when the dropdown menu items are loaded from AJAX requests, the items are created dynamically
const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className);
const $wrapper = $('<div>').append(menuItems);
const div = document.createElement('div');
div.innerHTML = menuItems;
const $wrapper = $(div);
const $items = $wrapper.find('> .item');
$items.each((_, item) => updateMenuItem($dropdown[0], item));
$dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem();