mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Use frontend fetch for branch dropdown component (#25719)
- Send request to get branch/tag list, use loading icon when waiting for response. - Only fetch when the first time branch/tag list shows. - For backend, removed assignment to `ctx.Data["Branches"]` and `ctx.Data["Tags"]` from `context/repo.go` and passed these data wherever needed. - Changed some `v-if` to `v-show` and used native `svg` as mentioned in https://github.com/go-gitea/gitea/pull/25719#issuecomment-1631712757 to improve perfomance when there are a lot of branches. - Places Used the dropdown component: Repo Home Page <img width="1429" alt="Screen Shot 2023-07-06 at 12 17 51" src="https://github.com/go-gitea/gitea/assets/17645053/6accc7b6-8d37-4e88-ae1a-bd2b3b927ea0"> Commits Page <img width="1431" alt="Screen Shot 2023-07-06 at 12 18 34" src="https://github.com/go-gitea/gitea/assets/17645053/2d0bf306-d1e2-45a8-a784-bc424879f537"> Specific commit -> operations -> cherry-pick <img width="758" alt="Screen Shot 2023-07-06 at 12 23 28" src="https://github.com/go-gitea/gitea/assets/17645053/1e557948-3881-4e45-a625-8ef36d45ae2d"> Release Page <img width="1433" alt="Screen Shot 2023-07-06 at 12 25 05" src="https://github.com/go-gitea/gitea/assets/17645053/3ec82af1-15a4-4162-a50b-04a9502161bb"> - Demo https://github.com/go-gitea/gitea/assets/17645053/d45d266b-3eb0-465a-82f9-57f78dc5f9f3 - Note: UI of dropdown menu could be improved in another PR as it should apply to more dropdown menus. Fix #14180 --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -660,13 +660,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 | 
			
		||||
		return cancel
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
		return cancel
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Tags"] = tags
 | 
			
		||||
 | 
			
		||||
	branchOpts := git_model.FindBranchOptions{
 | 
			
		||||
		RepoID:          ctx.Repo.Repository.ID,
 | 
			
		||||
		IsDeletedBranch: util.OptionalBoolFalse,
 | 
			
		||||
@@ -680,7 +673,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 | 
			
		||||
		return cancel
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// non empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
 | 
			
		||||
	// non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
 | 
			
		||||
	if branchesTotal == 0 { // fallback to do a sync immediately
 | 
			
		||||
		branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -689,24 +682,19 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: use paganation and async loading
 | 
			
		||||
	branchOpts.ExcludeBranchNames = []string{ctx.Repo.Repository.DefaultBranch}
 | 
			
		||||
	brs, err := git_model.FindBranchNames(ctx, branchOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetBranches", err)
 | 
			
		||||
		return cancel
 | 
			
		||||
	}
 | 
			
		||||
	// always put default branch on the top
 | 
			
		||||
	ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...)
 | 
			
		||||
	ctx.Data["BranchesCount"] = branchesTotal
 | 
			
		||||
 | 
			
		||||
	// If not branch selected, try default one.
 | 
			
		||||
	// If default branch doesn't exist, fall back to some other branch.
 | 
			
		||||
	// If no branch is set in the request URL, try to guess a default one.
 | 
			
		||||
	if len(ctx.Repo.BranchName) == 0 {
 | 
			
		||||
		if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
 | 
			
		||||
			ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
 | 
			
		||||
		} else if len(brs) > 0 {
 | 
			
		||||
			ctx.Repo.BranchName = brs[0]
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch()
 | 
			
		||||
			if ctx.Repo.BranchName == "" {
 | 
			
		||||
				// If it still can't get a default branch, fall back to default branch from setting.
 | 
			
		||||
				// Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
 | 
			
		||||
				ctx.Repo.BranchName = setting.Repository.DefaultBranch
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Repo.RefName = ctx.Repo.BranchName
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -754,6 +754,12 @@ func CompareDiff(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["HeadBranches"] = headBranches
 | 
			
		||||
 | 
			
		||||
	// For compare repo branches
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -785,18 +785,10 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	brs, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
 | 
			
		||||
		RepoID: ctx.Repo.Repository.ID,
 | 
			
		||||
		ListOptions: db.ListOptions{
 | 
			
		||||
			ListAll: true,
 | 
			
		||||
		},
 | 
			
		||||
		IsDeletedBranch: util.OptionalBoolFalse,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetBranches", err)
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Branches"] = brs
 | 
			
		||||
 | 
			
		||||
	// Contains true if the user can create issue dependencies
 | 
			
		||||
	ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull)
 | 
			
		||||
@@ -921,6 +913,13 @@ func NewIssue(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
 | 
			
		||||
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Tags"] = tags
 | 
			
		||||
 | 
			
		||||
	_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
 | 
			
		||||
	if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
 | 
			
		||||
		for k, v := range errs {
 | 
			
		||||
@@ -1918,6 +1917,19 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
 | 
			
		||||
		return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
 | 
			
		||||
	}
 | 
			
		||||
	// For sidebar
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Tags"] = tags
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplIssueView)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -729,6 +729,11 @@ func ViewPullCommits(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
 | 
			
		||||
	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
 | 
			
		||||
 | 
			
		||||
	// For PR commits page
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	getBranchData(ctx, issue)
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplPullCommits)
 | 
			
		||||
}
 | 
			
		||||
@@ -893,6 +898,11 @@ func ViewPullFiles(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
 | 
			
		||||
 | 
			
		||||
	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
 | 
			
		||||
	// For files changed page
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	upload.AddUploadContext(ctx, "comment")
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplPullFiles)
 | 
			
		||||
 
 | 
			
		||||
@@ -352,6 +352,20 @@ func NewRelease(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
 | 
			
		||||
 | 
			
		||||
	upload.AddUploadContext(ctx, "release")
 | 
			
		||||
 | 
			
		||||
	// For New Release page
 | 
			
		||||
	PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Tags"] = tags
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplReleaseNew)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -361,6 +375,13 @@ func NewReleasePost(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
 | 
			
		||||
	ctx.Data["PageIsReleaseList"] = true
 | 
			
		||||
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTagNamesByRepoID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Tags"] = tags
 | 
			
		||||
 | 
			
		||||
	if ctx.HasError() {
 | 
			
		||||
		ctx.HTML(http.StatusOK, tplReleaseNew)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -622,3 +622,64 @@ func SearchRepo(ctx *context.Context) {
 | 
			
		||||
		Data: results,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type branchTagSearchResponse struct {
 | 
			
		||||
	Results []string `json:"results"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetBranchesList get branches for current repo'
 | 
			
		||||
func GetBranchesList(ctx *context.Context) {
 | 
			
		||||
	branchOpts := git_model.FindBranchOptions{
 | 
			
		||||
		RepoID:          ctx.Repo.Repository.ID,
 | 
			
		||||
		IsDeletedBranch: util.OptionalBoolFalse,
 | 
			
		||||
		ListOptions: db.ListOptions{
 | 
			
		||||
			ListAll: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	branches, err := git_model.FindBranchNames(ctx, branchOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.JSON(http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp := &branchTagSearchResponse{}
 | 
			
		||||
	// always put default branch on the top if it exists
 | 
			
		||||
	if util.SliceContains(branches, ctx.Repo.Repository.DefaultBranch) {
 | 
			
		||||
		branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
 | 
			
		||||
	}
 | 
			
		||||
	resp.Results = branches
 | 
			
		||||
	ctx.JSON(http.StatusOK, resp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTagList get tag list for current repo
 | 
			
		||||
func GetTagList(ctx *context.Context) {
 | 
			
		||||
	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.JSON(http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp := &branchTagSearchResponse{}
 | 
			
		||||
	resp.Results = tags
 | 
			
		||||
	ctx.JSON(http.StatusOK, resp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrepareBranchList(ctx *context.Context) {
 | 
			
		||||
	branchOpts := git_model.FindBranchOptions{
 | 
			
		||||
		RepoID:          ctx.Repo.Repository.ID,
 | 
			
		||||
		IsDeletedBranch: util.OptionalBoolFalse,
 | 
			
		||||
		ListOptions: db.ListOptions{
 | 
			
		||||
			ListAll: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	brs, err := git_model.FindBranchNames(ctx, branchOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetBranches", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// always put default branch on the top if it exists
 | 
			
		||||
	if util.SliceContains(brs, ctx.Repo.Repository.DefaultBranch) {
 | 
			
		||||
		brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch)
 | 
			
		||||
		brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["Branches"] = brs
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/web/repo"
 | 
			
		||||
	"code.gitea.io/gitea/services/forms"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
	"code.gitea.io/gitea/services/repository"
 | 
			
		||||
@@ -44,6 +45,11 @@ func ProtectedBranchRules(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["ProtectedBranches"] = rules
 | 
			
		||||
 | 
			
		||||
	repo.PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplBranches)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -52,6 +58,11 @@ func SetDefaultBranchPost(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Tr("repo.settings.branches.update_default_branch")
 | 
			
		||||
	ctx.Data["PageIsSettingsBranches"] = true
 | 
			
		||||
 | 
			
		||||
	repo.PrepareBranchList(ctx)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo := ctx.Repo.Repository
 | 
			
		||||
 | 
			
		||||
	switch ctx.FormString("action") {
 | 
			
		||||
 
 | 
			
		||||
@@ -1094,6 +1094,7 @@ func registerRoutes(m *web.Route) {
 | 
			
		||||
		}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived())
 | 
			
		||||
 | 
			
		||||
		m.Group("/branches", func() {
 | 
			
		||||
			m.Get("/list", repo.GetBranchesList)
 | 
			
		||||
			m.Group("/_new", func() {
 | 
			
		||||
				m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
 | 
			
		||||
				m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
 | 
			
		||||
@@ -1108,6 +1109,7 @@ func registerRoutes(m *web.Route) {
 | 
			
		||||
	m.Group("/{username}/{reponame}", func() {
 | 
			
		||||
		m.Group("/tags", func() {
 | 
			
		||||
			m.Get("", repo.TagsList)
 | 
			
		||||
			m.Get("/list", repo.GetTagList)
 | 
			
		||||
			m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
 | 
			
		||||
			m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
 | 
			
		||||
		}, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
 | 
			
		||||
 
 | 
			
		||||
@@ -44,8 +44,6 @@
 | 
			
		||||
		'tagName': {{.root.TagName}},
 | 
			
		||||
		'branchName': {{.root.BranchName}},
 | 
			
		||||
		'noTag': {{.noTag}},
 | 
			
		||||
		'branches': {{.root.Branches}},
 | 
			
		||||
		'tags': {{.root.Tags}},
 | 
			
		||||
		'defaultBranch': {{$defaultBranch}},
 | 
			
		||||
		'enableFeed': {{.root.EnableFeed}},
 | 
			
		||||
		'rssURLPrefix': '{{$.root.RepoLink}}/rss/branch/',
 | 
			
		||||
 
 | 
			
		||||
@@ -3359,3 +3359,7 @@ tbody.commit-list {
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#cherry-pick-modal .scrolling.menu {
 | 
			
		||||
  max-height: 200px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
      </span>
 | 
			
		||||
      <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
 | 
			
		||||
    <div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
 | 
			
		||||
      <div class="ui icon search input">
 | 
			
		||||
        <i class="icon"><svg-icon name="octicon-filter" :size="16"/></i>
 | 
			
		||||
        <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder">
 | 
			
		||||
@@ -20,13 +20,13 @@
 | 
			
		||||
        <div class="header branch-tag-choice">
 | 
			
		||||
          <div class="ui grid">
 | 
			
		||||
            <div class="two column row">
 | 
			
		||||
              <a class="reference column" href="#" @click="createTag = false; mode = 'branches'; focusSearchField()">
 | 
			
		||||
              <a class="reference column" href="#" @click="handleTabSwitch('branches')">
 | 
			
		||||
                <span class="text" :class="{black: mode === 'branches'}">
 | 
			
		||||
                  <svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }}
 | 
			
		||||
                </span>
 | 
			
		||||
              </a>
 | 
			
		||||
              <template v-if="!noTag">
 | 
			
		||||
                <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
 | 
			
		||||
                <a class="reference column" href="#" @click="handleTabSwitch('tags')">
 | 
			
		||||
                  <span class="text" :class="{black: mode === 'tags'}">
 | 
			
		||||
                    <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }}
 | 
			
		||||
                  </span>
 | 
			
		||||
@@ -37,20 +37,23 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <div class="scrolling menu" ref="scrollContainer">
 | 
			
		||||
        <svg-icon name="octicon-rss" symbol-id="svg-symbol-octicon-rss"/>
 | 
			
		||||
        <div class="loading-indicator is-loading" v-if="isLoading"/>
 | 
			
		||||
        <div v-for="(item, index) in filteredItems" :key="item.name" class="item" :class="{selected: item.selected, active: active === index}" @click="selectItem(item)" :ref="'listItem' + index">
 | 
			
		||||
          {{ item.name }}
 | 
			
		||||
          <a v-if="enableFeed && mode === 'branches'" role="button" class="rss-icon ui compact right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
 | 
			
		||||
            <svg-icon name="octicon-rss" :size="14"/>
 | 
			
		||||
          <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon ui compact right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
 | 
			
		||||
            <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here -->
 | 
			
		||||
            <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
 | 
			
		||||
          <a href="#" @click="createNewBranch()">
 | 
			
		||||
            <div v-show="createTag">
 | 
			
		||||
            <div v-show="shouldCreateTag">
 | 
			
		||||
              <i class="reference tags icon"/>
 | 
			
		||||
              <!-- eslint-disable-next-line vue/no-v-html -->
 | 
			
		||||
              <span v-html="textCreateTag.replace('%s', searchTerm)"/>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div v-show="!createTag">
 | 
			
		||||
            <div v-show="!shouldCreateTag">
 | 
			
		||||
              <svg-icon name="octicon-git-branch"/>
 | 
			
		||||
              <!-- eslint-disable-next-line vue/no-v-html -->
 | 
			
		||||
              <span v-html="textCreateBranch.replace('%s', searchTerm)"/>
 | 
			
		||||
@@ -64,12 +67,12 @@
 | 
			
		||||
          <form ref="newBranchForm" :action="formActionUrl" method="post">
 | 
			
		||||
            <input type="hidden" name="_csrf" :value="csrfToken">
 | 
			
		||||
            <input type="hidden" name="new_branch_name" v-model="searchTerm">
 | 
			
		||||
            <input type="hidden" name="create_tag" v-model="createTag">
 | 
			
		||||
            <input type="hidden" name="create_tag" v-model="shouldCreateTag">
 | 
			
		||||
            <input type="hidden" name="current_path" v-model="treePath" v-if="treePath">
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="message" v-if="showNoResults">
 | 
			
		||||
      <div class="message" v-if="showNoResults && !isLoading">
 | 
			
		||||
        {{ noResults }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -81,6 +84,7 @@ import {createApp, nextTick} from 'vue';
 | 
			
		||||
import $ from 'jquery';
 | 
			
		||||
import {SvgIcon} from '../svg.js';
 | 
			
		||||
import {pathEscapeSegments} from '../utils/url.js';
 | 
			
		||||
import {showErrorToast} from '../modules/toast.js';
 | 
			
		||||
 | 
			
		||||
const sfc = {
 | 
			
		||||
  components: {SvgIcon},
 | 
			
		||||
@@ -110,12 +114,16 @@ const sfc = {
 | 
			
		||||
    formActionUrl() {
 | 
			
		||||
      return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`;
 | 
			
		||||
    },
 | 
			
		||||
    shouldCreateTag() {
 | 
			
		||||
      return this.mode === 'tags';
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  watch: {
 | 
			
		||||
    menuVisible(visible) {
 | 
			
		||||
      if (visible) {
 | 
			
		||||
        this.focusSearchField();
 | 
			
		||||
        this.fetchBranchesOrTags();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
@@ -139,7 +147,6 @@ const sfc = {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    selectItem(item) {
 | 
			
		||||
      const prev = this.getSelected();
 | 
			
		||||
@@ -246,7 +253,44 @@ const sfc = {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        this.menuVisible = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    handleTabSwitch(mode) {
 | 
			
		||||
      if (this.isLoading) return;
 | 
			
		||||
      this.mode = mode;
 | 
			
		||||
      this.focusSearchField();
 | 
			
		||||
      this.fetchBranchesOrTags();
 | 
			
		||||
    },
 | 
			
		||||
    async fetchBranchesOrTags() {
 | 
			
		||||
      if (!['branches', 'tags'].includes(this.mode) || this.isLoading) return;
 | 
			
		||||
      // only fetch when branch/tag list has not been initialized
 | 
			
		||||
      if (this.hasListInitialized[this.mode] ||
 | 
			
		||||
        (this.mode === 'branches' && !this.showBranchesInDropdown) ||
 | 
			
		||||
        (this.mode === 'tags' && this.noTag)
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.isLoading = true;
 | 
			
		||||
      try {
 | 
			
		||||
        // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name"
 | 
			
		||||
        const reqUrl = `${this.repoLink}/${this.mode}/list`;
 | 
			
		||||
        const resp = await fetch(reqUrl);
 | 
			
		||||
        const {results} = await resp.json();
 | 
			
		||||
        for (const result of results) {
 | 
			
		||||
          let selected = false;
 | 
			
		||||
          if (this.mode === 'branches') {
 | 
			
		||||
            selected = result === this.defaultBranch;
 | 
			
		||||
          } else {
 | 
			
		||||
            selected = result === (this.release ? this.release.tagName : this.defaultBranch);
 | 
			
		||||
          }
 | 
			
		||||
          this.items.push({name: result, url: pathEscapeSegments(result), branch: this.mode === 'branches', tag: this.mode === 'tags', selected});
 | 
			
		||||
        }
 | 
			
		||||
        this.hasListInitialized[this.mode] = true;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        showErrorToast(`Network error when fetching ${this.mode}, error: ${e}`);
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -258,7 +302,6 @@ export function initRepoBranchTagSelector(selector) {
 | 
			
		||||
      searchTerm: '',
 | 
			
		||||
      refNameText: '',
 | 
			
		||||
      menuVisible: false,
 | 
			
		||||
      createTag: false,
 | 
			
		||||
      release: null,
 | 
			
		||||
 | 
			
		||||
      isViewTag: false,
 | 
			
		||||
@@ -266,27 +309,15 @@ export function initRepoBranchTagSelector(selector) {
 | 
			
		||||
      isViewTree: false,
 | 
			
		||||
 | 
			
		||||
      active: 0,
 | 
			
		||||
 | 
			
		||||
      isLoading: false,
 | 
			
		||||
      // This means whether branch list/tag list has initialized
 | 
			
		||||
      hasListInitialized: {
 | 
			
		||||
        'branches': false,
 | 
			
		||||
        'tags': false,
 | 
			
		||||
      },
 | 
			
		||||
      ...window.config.pageData.branchDropdownDataList[elIndex],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name"
 | 
			
		||||
 | 
			
		||||
    if (data.showBranchesInDropdown && data.branches) {
 | 
			
		||||
      for (const branch of data.branches) {
 | 
			
		||||
        data.items.push({name: branch, url: pathEscapeSegments(branch), branch: true, tag: false, selected: branch === data.defaultBranch});
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!data.noTag && data.tags) {
 | 
			
		||||
      for (const tag of data.tags) {
 | 
			
		||||
        if (data.release) {
 | 
			
		||||
          data.items.push({name: tag, url: pathEscapeSegments(tag), branch: false, tag: true, selected: tag === data.release.tagName});
 | 
			
		||||
        } else {
 | 
			
		||||
          data.items.push({name: tag, url: pathEscapeSegments(tag), branch: false, tag: true, selected: tag === data.defaultBranch});
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const comp = {...sfc, data() { return data }};
 | 
			
		||||
    createApp(comp).mount(elRoot);
 | 
			
		||||
  }
 | 
			
		||||
@@ -302,4 +333,8 @@ export default sfc; // activate IDE's Vue plugin
 | 
			
		||||
.menu .item:hover .rss-icon {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrolling.menu .loading-indicator {
 | 
			
		||||
  height: 4em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import {htmlEscape} from 'escape-goat';
 | 
			
		||||
import {svg} from '../svg.js';
 | 
			
		||||
import Toastify from 'toastify-js';
 | 
			
		||||
 | 
			
		||||
const levels = {
 | 
			
		||||
  info: {
 | 
			
		||||
@@ -23,7 +24,6 @@ const levels = {
 | 
			
		||||
async function showToast(message, level, {gravity, position, duration, ...other} = {}) {
 | 
			
		||||
  if (!message) return;
 | 
			
		||||
 | 
			
		||||
  const {default: Toastify} = await import(/* webpackChunkName: 'toastify' */'toastify-js');
 | 
			
		||||
  const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
 | 
			
		||||
 | 
			
		||||
  const toast = Toastify({
 | 
			
		||||
 
 | 
			
		||||
@@ -185,9 +185,10 @@ export const SvgIcon = {
 | 
			
		||||
    name: {type: String, required: true},
 | 
			
		||||
    size: {type: Number, default: 16},
 | 
			
		||||
    className: {type: String, default: ''},
 | 
			
		||||
    symbolId: {type: String}
 | 
			
		||||
  },
 | 
			
		||||
  render() {
 | 
			
		||||
    const {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
 | 
			
		||||
    let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
 | 
			
		||||
    // https://vuejs.org/guide/extras/render-function.html#creating-vnodes
 | 
			
		||||
    // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc
 | 
			
		||||
    const attrs = {};
 | 
			
		||||
@@ -207,7 +208,10 @@ export const SvgIcon = {
 | 
			
		||||
    if (this.className) {
 | 
			
		||||
      classes.push(...this.className.split(/\s+/).filter(Boolean));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.symbolId) {
 | 
			
		||||
      classes.push('gt-hidden', 'svg-symbol-container');
 | 
			
		||||
      svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
 | 
			
		||||
    }
 | 
			
		||||
    // create VNode
 | 
			
		||||
    return h('svg', {
 | 
			
		||||
      ...attrs,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user