From 3b91b2d6b12b9c9c18406f484775925bbd557618 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 23 Aug 2023 09:56:11 +0800 Subject: [PATCH 01/21] add mfa doc (#26654) copy and modified from #14572 > Whilst debating enforcing MFA within our team, I realised there isn't a lot of context to the side effects of enabling it. Most of us use Git over HTTP and would need to add a token. I plan to add another PR that adds a sentence to the UI about needing to generate a token when enabling MFA if HTTP is to be used. --------- Co-authored-by: techknowlogick Co-authored-by: silverwind --- .../multi-factor-authentication.en-us.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/content/usage/multi-factor-authentication.en-us.md diff --git a/docs/content/usage/multi-factor-authentication.en-us.md b/docs/content/usage/multi-factor-authentication.en-us.md new file mode 100644 index 0000000000000..16b57b7bdca74 --- /dev/null +++ b/docs/content/usage/multi-factor-authentication.en-us.md @@ -0,0 +1,35 @@ +--- +date: "2023-08-22T14:21:00+08:00" +title: "Usage: Multi-factor Authentication (MFA)" +slug: "multi-factor-authentication" +weight: 15 +toc: false +draft: false +menu: + sidebar: + parent: "usage" + name: "Multi-factor Authentication (MFA)" + weight: 15 + identifier: "multi-factor-authentication" +--- + +# Multi-factor Authentication (MFA) + +Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password. +If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure. +Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API. + +MFA can be configured within the "Security" tab of the user settings page. + +## MFA Considerations + +Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI. +This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled. +If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page. +This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP. + +> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA. +> It must be kept secure and should only be used as a last resort. + +The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md). +This should be used instead of an access token where possible. From 5db21ce7e10ba78ede8841bea9db7a63adbececb Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 23 Aug 2023 10:29:17 +0800 Subject: [PATCH 02/21] Fix counting and filtering on the dashboard page for issues (#26657) This PR has multiple parts, and I didn't split them because it's not easy to test them separately since they are all about the dashboard page for issues. 1. Support counting issues via indexer to fix #26361 2. Fix repo selection so it also fixes #26653 3. Keep keywords in filter links. The first two are regressions of #26012. After: https://github.com/go-gitea/gitea/assets/9418365/71dfea7e-d9e2-42b6-851a-cc081435c946 Thanks to @CaiCandong for helping with some tests. --- models/repo/repo_list.go | 36 +++-- modules/indexer/issues/indexer.go | 41 ++++- modules/indexer/issues/internal/model.go | 13 ++ routers/web/user/home.go | 195 +++++++++++++---------- templates/user/dashboard/issues.tmpl | 12 +- 5 files changed, 187 insertions(+), 110 deletions(-) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index b8427bec4eb45..a0485ed8d417d 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -130,6 +130,10 @@ type SearchRepoOptions struct { // True -> include just collaborative // False -> include just non-collaborative Collaborate util.OptionalBool + // What type of unit the user can be collaborative in, + // it is ignored if Collaborate is False. + // TypeInvalid means any unit type. + UnitType unit.Type // None -> include forks AND non-forks // True -> include just forks // False -> include just non-forks @@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Collaborate != util.OptionalBoolFalse { // A Collaboration is: - collaborateCond := builder.And( - // 1. Repository we don't own - builder.Neq{"owner_id": opts.OwnerID}, - // 2. But we can see because of: - builder.Or( - // A. We have unit independent access - UserAccessRepoCond("`repository`.id", opts.OwnerID), - // B. We are in a team for - UserOrgTeamRepoCond("`repository`.id", opts.OwnerID), - // C. Public repositories in organizations that we are member of - userOrgPublicRepoCondPrivate(opts.OwnerID), - ), - ) + + collaborateCond := builder.NewCond() + // 1. Repository we don't own + collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID}) + // 2. But we can see because of: + { + userAccessCond := builder.NewCond() + // A. We have unit independent access + userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID)) + // B. We are in a team for + if opts.UnitType == unit.TypeInvalid { + userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID)) + } else { + userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType)) + } + // C. Public repositories in organizations that we are member of + userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID)) + collaborateCond = collaborateCond.And(userAccessCond) + } if !opts.Private { collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) } diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 6619949104f49..020659c82b541 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -13,6 +13,7 @@ import ( db_model "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/indexer/issues/bleve" "code.gitea.io/gitea/modules/indexer/issues/db" @@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool { } // SearchOptions indicates the options for searching issues -type SearchOptions internal.SearchOptions +type SearchOptions = internal.SearchOptions const ( SortByCreatedDesc = internal.SortByCreatedDesc @@ -291,7 +292,6 @@ const ( ) // SearchIssues search issues by options. -// It returns issue ids and a bool value indicates if the result is imprecise. func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) { indexer := *globalIndexer.Load() @@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err indexer = db.NewIndexer() } - result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts)) + result, err := indexer.Search(ctx, opts) if err != nil { return nil, 0, err } @@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err return ret, result.Total, nil } + +// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count. +func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) { + opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} }) + + _, total, err := SearchIssues(ctx, opts) + return total, err +} + +// CountIssuesByRepo counts issues by options and group by repo id. +// It's not a complete implementation, since it requires the caller should provide the repo ids. +// That means opts.RepoIDs must be specified, and opts.AllPublic must be false. +// It's good enough for the current usage, and it can be improved if needed. +// TODO: use "group by" of the indexer engines to implement it. +func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) { + if len(opts.RepoIDs) == 0 { + return nil, fmt.Errorf("opts.RepoIDs must be specified") + } + if opts.AllPublic { + return nil, fmt.Errorf("opts.AllPublic must be false") + } + + repoIDs := container.SetOf(opts.RepoIDs...).Values() + ret := make(map[int64]int64, len(repoIDs)) + // TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow. + for _, repoID := range repoIDs { + count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} })) + if err != nil { + return nil, err + } + ret[repoID] = count + } + + return ret, nil +} diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 2de1e0e2bf20b..031745dd2fcc1 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -109,6 +109,19 @@ type SearchOptions struct { SortBy SortBy // sort by field } +// Copy returns a copy of the options. +// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not. +func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions { + if o == nil { + return nil + } + v := *o + for _, e := range edit { + e(&v) + } + return &v +} + type SortBy string const ( diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 8c1447f707863..d1a4877e6d4b6 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -448,21 +448,26 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // - Team org's owns the repository. // - Team has read permission to repository. repoOpts := &repo_model.SearchRepoOptions{ - Actor: ctx.Doer, - OwnerID: ctx.Doer.ID, - Private: true, - AllPublic: false, - AllLimited: false, + Actor: ctx.Doer, + OwnerID: ctx.Doer.ID, + Private: true, + AllPublic: false, + AllLimited: false, + Collaborate: util.OptionalBoolNone, + UnitType: unitType, + Archived: util.OptionalBoolFalse, } if team != nil { repoOpts.TeamID = team.ID } + accessibleRepos := container.Set[int64]{} { ids, _, err := repo_model.SearchRepositoryIDs(repoOpts) if err != nil { ctx.ServerError("SearchRepositoryIDs", err) return } + accessibleRepos.AddMultiple(ids...) opts.RepoIDs = ids if len(opts.RepoIDs) == 0 { // no repos found, don't let the indexer return all repos @@ -489,40 +494,16 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { keyword := strings.Trim(ctx.FormString("q"), " ") ctx.Data["Keyword"] = keyword - accessibleRepos := container.Set[int64]{} - { - ids, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser) - if err != nil { - ctx.ServerError("GetRepoIDsForIssuesOptions", err) - return - } - for _, id := range ids { - accessibleRepos.Add(id) - } - } - // Educated guess: Do or don't show closed issues. isShowClosed := ctx.FormString("state") == "closed" opts.IsClosed = util.OptionalBoolOf(isShowClosed) // Filter repos and count issues in them. Count will be used later. // USING NON-FINAL STATE OF opts FOR A QUERY. - var issueCountByRepo map[int64]int64 - { - issueIDs, err := issueIDsFromSearch(ctx, keyword, opts) - if err != nil { - ctx.ServerError("issueIDsFromSearch", err) - return - } - if len(issueIDs) > 0 { // else, no issues found, just leave issueCountByRepo empty - opts.IssueIDs = issueIDs - issueCountByRepo, err = issues_model.CountIssuesByRepo(ctx, opts) - if err != nil { - ctx.ServerError("CountIssuesByRepo", err) - return - } - opts.IssueIDs = nil // reset, the opts will be used later - } + issueCountByRepo, err := issue_indexer.CountIssuesByRepo(ctx, issue_indexer.ToSearchOptions(keyword, opts)) + if err != nil { + ctx.ServerError("CountIssuesByRepo", err) + return } // Make sure page number is at least 1. Will be posted to ctx.Data. @@ -551,13 +532,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Parse ctx.FormString("repos") and remember matched repo IDs for later. // Gets set when clicking filters on the issues overview page. - repoIDs := getRepoIDs(ctx.FormString("repos")) - if len(repoIDs) > 0 { - // Remove repo IDs that are not accessible to the user. - repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool { - return !accessibleRepos.Contains(v) - }) - opts.RepoIDs = repoIDs + selectedRepoIDs := getRepoIDs(ctx.FormString("repos")) + // Remove repo IDs that are not accessible to the user. + selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool { + return !accessibleRepos.Contains(v) + }) + if len(selectedRepoIDs) > 0 { + opts.RepoIDs = selectedRepoIDs } // ------------------------------ @@ -568,7 +549,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // USING FINAL STATE OF opts FOR A QUERY. var issues issues_model.IssueList { - issueIDs, err := issueIDsFromSearch(ctx, keyword, opts) + issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts)) if err != nil { ctx.ServerError("issueIDsFromSearch", err) return @@ -584,6 +565,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Add repository pointers to Issues. // ---------------------------------- + // Remove repositories that should not be shown, + // which are repositories that have no issues and are not selected by the user. + selectedReposMap := make(map[int64]struct{}, len(selectedRepoIDs)) + for _, repoID := range selectedRepoIDs { + selectedReposMap[repoID] = struct{}{} + } + for k, v := range issueCountByRepo { + if _, ok := selectedReposMap[k]; !ok && v == 0 { + delete(issueCountByRepo, k) + } + } + // showReposMap maps repository IDs to their Repository pointers. showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType) if err != nil { @@ -615,44 +608,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - var issueStats *issues_model.IssueStats - { - statsOpts := issues_model.IssuesOptions{ - RepoIDs: repoIDs, - User: ctx.Doer, - IsPull: util.OptionalBoolOf(isPullList), - IsClosed: util.OptionalBoolOf(isShowClosed), - IssueIDs: nil, - IsArchived: util.OptionalBoolFalse, - LabelIDs: opts.LabelIDs, - Org: org, - Team: team, - RepoCond: opts.RepoCond, - } - - if keyword != "" { - statsOpts.RepoIDs = opts.RepoIDs - allIssueIDs, err := issueIDsFromSearch(ctx, keyword, &statsOpts) - if err != nil { - ctx.ServerError("issueIDsFromSearch", err) - return - } - statsOpts.IssueIDs = allIssueIDs - } - - if keyword != "" && len(statsOpts.IssueIDs) == 0 { - // So it did search with the keyword, but no issue found. - // Just set issueStats to empty. - issueStats = &issues_model.IssueStats{} - } else { - // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues. - // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts. - issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts) - if err != nil { - ctx.ServerError("GetUserIssueStats", err) - return - } - } + issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts), ctx.Doer.ID) + if err != nil { + ctx.ServerError("getUserIssueStats", err) + return } // Will be posted to ctx.Data. @@ -722,7 +681,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["IssueStats"] = issueStats ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType - ctx.Data["RepoIDs"] = opts.RepoIDs + ctx.Data["RepoIDs"] = selectedRepoIDs ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["SelectLabels"] = selectedLabels @@ -777,14 +736,6 @@ func getRepoIDs(reposQuery string) []int64 { return repoIDs } -func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) { - ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts)) - if err != nil { - return nil, fmt.Errorf("SearchIssues: %w", err) - } - return ids, nil -} - func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) { totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo)) repoIDs := make([]int64, 0, 500) @@ -913,3 +864,71 @@ func UsernameSubRoute(ctx *context.Context) { } } } + +func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions, doerID int64) (*issues_model.IssueStats, error) { + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + o.AssigneeID = nil + o.PosterID = nil + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }) + + var ( + err error + ret = &issues_model.IssueStats{} + ) + + { + openClosedOpts := opts.Copy() + switch filterMode { + case issues_model.FilterModeAll, issues_model.FilterModeYourRepositories: + case issues_model.FilterModeAssign: + openClosedOpts.AssigneeID = &doerID + case issues_model.FilterModeCreate: + openClosedOpts.PosterID = &doerID + case issues_model.FilterModeMention: + openClosedOpts.MentionID = &doerID + case issues_model.FilterModeReviewRequested: + openClosedOpts.ReviewRequestedID = &doerID + case issues_model.FilterModeReviewed: + openClosedOpts.ReviewedID = &doerID + } + openClosedOpts.IsClosed = util.OptionalBoolFalse + ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) + if err != nil { + return nil, err + } + openClosedOpts.IsClosed = util.OptionalBoolTrue + ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) + if err != nil { + return nil, err + } + } + + ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts) + if err != nil { + return nil, err + } + ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID })) + if err != nil { + return nil, err + } + ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID })) + if err != nil { + return nil, err + } + ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID })) + if err != nil { + return nil, err + } + ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID })) + if err != nil { + return nil, err + } + ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID })) + if err != nil { + return nil, err + } + return ret, nil +} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 8d6cc67afe17e..a89098c6ab31b 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -5,29 +5,29 @@
Very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content + Truncate very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index aad44f57dff4e..6bfd8b12be710 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -80,25 +80,21 @@ {{end}}
{{if or (eq .GetOpType 5) (eq .GetOpType 18)}} -
- {{$push := ActionContent2Commits .}} - {{$repoLink := .GetRepoLink}} - {{range $push.Commits}} - {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} -
- - {{ShortSha .Sha1}} - - {{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}} - -
- {{end}} - {{if and (gt $push.Len 1) $push.CompareURL}} - - {{end}} -
+ {{$push := ActionContent2Commits .}} + {{$repoLink := .GetRepoLink}} + {{range $push.Commits}} + {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} +
+ + {{ShortSha .Sha1}} + + {{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}} + +
+ {{end}} + {{if and (gt $push.Len 1) $push.CompareURL}} + {{$.locale.Tr "action.compare_commits" $push.Len}} » + {{end}} {{else if eq .GetOpType 6}} {{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}} {{else if eq .GetOpType 7}} diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 1eb480845b0d4..402eb7b34b431 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -96,10 +96,6 @@ } } -.feeds .commit-id { - font-family: var(--fonts-monospace); -} - .feeds code { padding: 2px 4px; border-radius: 3px; diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index ec22fbba9e21c..c73f78ebfe035 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -29,7 +29,8 @@ display: flex; flex-direction: column; flex-grow: 1; - flex-basis: 60%; + flex-basis: 60%; /* avoid wrapping the "flex-item-trailing" too aggressively */ + min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ } .flex-item-header { @@ -66,6 +67,7 @@ font-size: 16px; font-weight: var(--font-weight-semibold); word-break: break-word; + min-width: 0; } .flex-item .flex-item-title a { From a428591f6b86d4ece21292712a9a5491266303eb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 23 Aug 2023 15:25:13 +0800 Subject: [PATCH 04/21] Refactor toast module (#26677) 1. Do not use "async" 2. Call `hideToast` instead of `removeElement` for manual closing --- web_src/js/features/common-global.js | 6 +++--- web_src/js/modules/toast.js | 23 +++++++++-------------- web_src/js/modules/toast.test.js | 6 +++--- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 7291410c1a30b..d02a82a2efcb7 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -95,14 +95,14 @@ async function fetchActionDoRequest(actionElem, url, opt) { const data = await resp.json(); // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. - await showErrorToast(data.errorMessage || `server error: ${resp.status}`); + showErrorToast(data.errorMessage || `server error: ${resp.status}`); } else { - await showErrorToast(`server error: ${resp.status}`); + showErrorToast(`server error: ${resp.status}`); } } catch (e) { console.error('error when doRequest', e); actionElem.classList.remove('is-loading', 'small-loading-icon'); - await showErrorToast(i18n.network_error); + showErrorToast(i18n.network_error); } } diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js index b5899052d48c6..fa075aed4843e 100644 --- a/web_src/js/modules/toast.js +++ b/web_src/js/modules/toast.js @@ -1,6 +1,6 @@ import {htmlEscape} from 'escape-goat'; import {svg} from '../svg.js'; -import Toastify from 'toastify-js'; +import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown const levels = { info: { @@ -21,9 +21,7 @@ const levels = { }; // See https://github.com/apvarun/toastify-js#api for options -async function showToast(message, level, {gravity, position, duration, ...other} = {}) { - if (!message) return; - +function showToast(message, level, {gravity, position, duration, ...other} = {}) { const {icon, background, duration: levelDuration} = levels[level ?? 'info']; const toast = Toastify({ @@ -41,20 +39,17 @@ async function showToast(message, level, {gravity, position, duration, ...other} }); toast.showToast(); - - toast.toastElement.querySelector('.toast-close').addEventListener('click', () => { - toast.removeElement(toast.toastElement); - }); + toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); } -export async function showInfoToast(message, opts) { - return await showToast(message, 'info', opts); +export function showInfoToast(message, opts) { + return showToast(message, 'info', opts); } -export async function showWarningToast(message, opts) { - return await showToast(message, 'warning', opts); +export function showWarningToast(message, opts) { + return showToast(message, 'warning', opts); } -export async function showErrorToast(message, opts) { - return await showToast(message, 'error', opts); +export function showErrorToast(message, opts) { + return showToast(message, 'error', opts); } diff --git a/web_src/js/modules/toast.test.js b/web_src/js/modules/toast.test.js index b691aaebb634a..b5066df0b2889 100644 --- a/web_src/js/modules/toast.test.js +++ b/web_src/js/modules/toast.test.js @@ -2,16 +2,16 @@ import {test, expect} from 'vitest'; import {showInfoToast, showErrorToast, showWarningToast} from './toast.js'; test('showInfoToast', async () => { - await showInfoToast('success 😀', {duration: -1}); + showInfoToast('success 😀', {duration: -1}); expect(document.querySelector('.toastify')).toBeTruthy(); }); test('showWarningToast', async () => { - await showWarningToast('warning 😐', {duration: -1}); + showWarningToast('warning 😐', {duration: -1}); expect(document.querySelector('.toastify')).toBeTruthy(); }); test('showErrorToast', async () => { - await showErrorToast('error 🙁', {duration: -1}); + showErrorToast('error 🙁', {duration: -1}); expect(document.querySelector('.toastify')).toBeTruthy(); }); From af33a1187b0ed7cf1ffbe8aabddf7ed0380f2cc9 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 23 Aug 2023 16:13:04 +0800 Subject: [PATCH 05/21] Fix doubled box-shadow in branch dropdown menu (#26678) --- templates/repo/branch_dropdown.tmpl | 2 +- web_src/js/components/RepoBranchTagSelector.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 4ec2fd557c76a..a011111d5b7cb 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -67,7 +67,7 @@
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} -