diff options
author | HesterG <hestergong@gmail.com> | 2023-07-21 19:20:04 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-21 11:20:04 +0000 |
commit | 2f0e79e6393df13930eaa419273d24dc2ef36cfa (patch) | |
tree | 878d19f8055f7f5c5ce114620c05d18cbfb6c79a /web_src/js | |
parent | dbbae67f44364eb965f516bfc77ba25dd5242c16 (diff) | |
download | gitea-2f0e79e6393df13930eaa419273d24dc2ef36cfa.tar.gz gitea-2f0e79e6393df13930eaa419273d24dc2ef36cfa.zip |
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>
Diffstat (limited to 'web_src/js')
-rw-r--r-- | web_src/js/components/RepoBranchTagSelector.vue | 95 | ||||
-rw-r--r-- | web_src/js/modules/toast.js | 2 | ||||
-rw-r--r-- | web_src/js/svg.js | 8 |
3 files changed, 72 insertions, 33 deletions
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 4fc3936244..e6e72e3886 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -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> diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js index b0d02dc644..b5899052d4 100644 --- a/web_src/js/modules/toast.js +++ b/web_src/js/modules/toast.js @@ -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({ diff --git a/web_src/js/svg.js b/web_src/js/svg.js index b0c55e4e37..2ef839aa21 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -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, |