mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01: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:
		| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user