loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor branch/tag selector to Vue SFC (#23421)

Follow #23394

There were many bad smells in old code. This PR only moves the code into
Vue SFC, doesn't touch the unrelated logic.

update: after
https://github.com/go-gitea/gitea/pull/23421/commits/5f23218c851e12132f538a404c946bbf6ff38e62
, there should be no usage of the vue-rumtime-compiler anymore
(hopefully), so I think this PR could close #19851

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>

authored by

wxiaoguang
Lunny Xiao
and committed by
GitHub
ac8d71ff d56bb742

+359 -309
+32 -66
templates/repo/branch_dropdown.tmpl
··· 1 - {{$release := .release}} 2 - {{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}} 3 - {{$type := ""}}{{if and .root.IsViewTag (not .noTag)}}{{$type = "tag"}}{{else if .root.IsViewBranch}}{{$type = "branch"}}{{else}}{{$type = "tree"}}{{end}} 1 + {{$defaultBranch := $.root.BranchName}} 2 + {{if and .root.IsViewTag (not .noTag)}} 3 + {{$defaultBranch = .root.TagName}} 4 + {{end}} 5 + {{if eq $defaultBranch ""}} 6 + {{$defaultBranch = $.root.Repository.DefaultBranch}} 7 + {{end}} 8 + 9 + {{$type := ""}} 10 + {{if and .root.IsViewTag (not .noTag)}} 11 + {{$type = "tag"}} 12 + {{else if .root.IsViewBranch}} 13 + {{$type = "branch"}} 14 + {{else}} 15 + {{$type = "tree"}} 16 + {{end}} 17 + 4 18 {{$showBranchesInDropdown := not .root.HideBranchesInDropdown}} 5 19 6 20 <script type="module"> ··· 30 44 'defaultBranch': {{$defaultBranch}}, 31 45 'branchURLPrefix': '{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}', 32 46 'branchURLSuffix': '{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}', 33 - 'tagURLPrefix': '{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if $release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}', 34 - 'tagURLSuffix': '{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if $release}}...{{if $release.IsDraft}}{{PathEscapeSegments $release.Target}}{{else}}{{if $release.TagName}}{{PathEscapeSegments $release.TagName}}{{else}}{{PathEscapeSegments $release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}', 47 + 'tagURLPrefix': '{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if .release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}', 48 + 'tagURLSuffix': '{{if .tagURLSuffix}}{{.tagURLSuffix}}{{else if .release}}...{{if .release.IsDraft}}{{PathEscapeSegments .release.Target}}{{else}}{{if .release.TagName}}{{PathEscapeSegments .release.TagName}}{{else}}{{PathEscapeSegments .release.Sha1}}{{end}}{{end}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}', 35 49 'repoLink': {{.root.RepoLink}}, 36 50 'treePath': {{.root.TreePath}}, 37 51 'branchNameSubURL': {{.root.BranchNameSubURL}}, ··· 46 60 window.config.pageData.branchDropdownDataList.push(data); 47 61 </script> 48 62 49 - <div class="fitted item choose reference"> 63 + <div class="fitted item js-branch-tag-selector"> 64 + {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} 50 65 <div class="ui floating filter dropdown custom"> 51 - <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> 66 + <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df"> 52 67 <span class="text gt-df gt-ac gt-mr-2"> 53 - {{/* v-cloak is used to hide unnecessary elements before Vue componment is mounted */}} 54 - <span v-cloak v-if="release">${ textReleaseCompare }</span> 55 - <span :class="{visible: isViewTag}" v-if="isViewTag" {{if not (eq $type "tag")}}v-cloak{{end}}>{{svg "octicon-tag"}}</span> 56 - <span :class="{visible: isViewBranch}" v-if="isViewBranch" {{if not (eq $type "branch")}}v-cloak{{end}}>{{svg "octicon-git-branch"}}</span> 57 - <span :class="{visible: isViewTree}" v-if="isViewTree" {{if not (eq $type "tree")}}v-cloak{{end}}>{{svg "octicon-git-branch"}}</span> 58 - <strong ref="dropdownRefName" class="gt-ml-3">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong> 68 + {{if .release}} 69 + {{.root.locale.Tr "repo.release.compare"}} 70 + {{else}} 71 + {{if eq $type "tag"}} 72 + {{svg "octicon-tag"}} 73 + {{else}} 74 + {{svg "octicon-git-branch"}} 75 + {{end}} 76 + <strong ref="dropdownRefName" class="gt-ml-3">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong> 77 + {{end}} 59 78 </span> 60 79 {{svg "octicon-triangle-down" 14 "dropdown icon"}} 61 80 </button> 62 - <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak> 63 - <div class="ui icon search input"> 64 - <i class="icon gt-df gt-ac gt-jc gt-m-0">{{svg "octicon-filter" 16}}</i> 65 - <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder"> 66 - </div> 67 - <template v-if="showBranchesInDropdown"> 68 - <div class="header branch-tag-choice"> 69 - <div class="ui grid"> 70 - <div class="two column row"> 71 - <a class="reference column" href="#" @click="createTag = false; mode = 'branches'; focusSearchField()"> 72 - <span class="text" :class="{black: mode === 'branches'}"> 73 - {{svg "octicon-git-branch" 16 "gt-mr-2"}}${ textBranches } 74 - </span> 75 - </a> 76 - <template v-if="!noTag"> 77 - <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()"> 78 - <span class="text" :class="{black: mode === 'tags'}"> 79 - {{svg "octicon-tag" 16 "gt-mr-2"}}${ textTags } 80 - </span> 81 - </a> 82 - </template> 83 - </div> 84 - </div> 85 - </div> 86 - </template> 87 - <div class="scrolling menu" ref="scrollContainer"> 88 - <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 }</div> 89 - <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length"> 90 - <a href="#" @click="createNewBranch()"> 91 - <div v-show="createTag"> 92 - <i class="reference tags icon"></i> 93 - <span v-html="textCreateTag.replace('%s', searchTerm)"></span> 94 - </div> 95 - <div v-show="!createTag"> 96 - {{svg "octicon-git-branch"}} 97 - <span v-html="textCreateBranch.replace('%s', searchTerm)"></span> 98 - </div> 99 - <div class="text small"> 100 - <span v-if="isViewBranch || release">${ textCreateBranchFrom.replace('%s', branchName) }</span> 101 - <span v-else-if="isViewTag">${ textCreateBranchFrom.replace('%s', tagName) }</span> 102 - <span v-else>${ textCreateBranchFrom.replace('%s', commitIdShort) }</span> 103 - </div> 104 - </a> 105 - <form ref="newBranchForm" action="{{.root.RepoLink}}/branches/_new/{{.root.BranchNameSubURL}}" method="post"> 106 - <input type="hidden" name="_csrf" :value="csrfToken"> 107 - <input type="hidden" name="new_branch_name" v-model="searchTerm"> 108 - <input type="hidden" name="create_tag" v-model="createTag"> 109 - <input type="hidden" name="current_path" v-model="treePath" v-if="treePath"> 110 - </form> 111 - </div> 112 - </div> 113 - <div class="message" v-if="showNoResults">${ noResults }</div> 114 - </div> 115 81 </div> 116 82 </div>
+1 -1
web_src/js/components/DashboardRepoList.vue
··· 73 73 <li v-for="repo in repos" :class="{'private': repo.private || repo.internal}" :key="repo.id"> 74 74 <a class="repo-list-link gt-df gt-ac gt-sb" :href="repo.link"> 75 75 <div class="item-name gt-df gt-ac gt-f1 gt-mr-2"> 76 - <svg-icon :name="repoIcon(repo)" size="16" class-name="gt-mr-2"/> 76 + <svg-icon :name="repoIcon(repo)" :size="16" class-name="gt-mr-2"/> 77 77 <div class="text gt-bold truncate gt-ml-1">{{ repo.full_name }}</div> 78 78 <span v-if="repo.archived"> 79 79 <svg-icon name="octicon-archive" :size="16" class-name="gt-ml-2"/>
+4 -3
web_src/js/components/PullRequestMergeForm.vue
··· 10 10 -d '{"context": "test/context", "description": "description", "state": "${state}", "target_url": "http://localhost"}' 11 11 --> 12 12 <div> 13 - <!-- eslint-disable --> 14 - <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"></div> 13 + <!-- eslint-disable-next-line vue/no-v-html --> 14 + <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/> 15 15 16 16 <div class="ui form" v-if="showActionForm"> 17 17 <form :action="mergeForm.baseLink+'/merge'" method="post"> ··· 30 30 <button @click.prevent="clearMergeMessage" class="ui tertiary button"> 31 31 {{ mergeForm.textClearMergeMessage }} 32 32 </button> 33 - <div class="ui label"><!-- TODO: Convert to tooltip once we can use tooltips in Vue templates --> 33 + <div class="ui label"> 34 + <!-- TODO: Convert to tooltip once we can use tooltips in Vue templates --> 34 35 {{ mergeForm.textClearMergeMessageHint }} 35 36 </div> 36 37 </template>
-208
web_src/js/components/RepoBranchTagDropdown.js
··· 1 - import {createApp, nextTick} from 'vue'; 2 - import $ from 'jquery'; 3 - 4 - export function initRepoBranchTagDropdown(selector) { 5 - $(selector).each(function (dropdownIndex, elRoot) { 6 - const data = { 7 - csrfToken: window.config.csrfToken, 8 - items: [], 9 - searchTerm: '', 10 - menuVisible: false, 11 - createTag: false, 12 - release: null, 13 - 14 - isViewTag: false, 15 - isViewBranch: false, 16 - isViewTree: false, 17 - 18 - active: 0, 19 - 20 - ...window.config.pageData.branchDropdownDataList[dropdownIndex], 21 - }; 22 - 23 - // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name" 24 - 25 - if (data.showBranchesInDropdown && data.branches) { 26 - for (const branch of data.branches) { 27 - data.items.push({name: branch, url: branch, branch: true, tag: false, selected: branch === data.defaultBranch}); 28 - } 29 - } 30 - if (!data.noTag && data.tags) { 31 - for (const tag of data.tags) { 32 - if (data.release) { 33 - data.items.push({name: tag, url: tag, branch: false, tag: true, selected: tag === data.release.tagName}); 34 - } else { 35 - data.items.push({name: tag, url: tag, branch: false, tag: true, selected: tag === data.defaultBranch}); 36 - } 37 - } 38 - } 39 - 40 - const view = createApp({ 41 - delimiters: ['${', '}'], 42 - data() { 43 - return data; 44 - }, 45 - computed: { 46 - filteredItems() { 47 - const items = this.items.filter((item) => { 48 - return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && 49 - (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); 50 - }); 51 - 52 - // no idea how to fix this so linting rule is disabled instead 53 - this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties 54 - return items; 55 - }, 56 - showNoResults() { 57 - return this.filteredItems.length === 0 && !this.showCreateNewBranch; 58 - }, 59 - showCreateNewBranch() { 60 - if (this.disableCreateBranch || !this.searchTerm) { 61 - return false; 62 - } 63 - 64 - return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; 65 - } 66 - }, 67 - 68 - watch: { 69 - menuVisible(visible) { 70 - if (visible) { 71 - this.focusSearchField(); 72 - } 73 - } 74 - }, 75 - 76 - beforeMount() { 77 - switch (data.viewType) { 78 - case 'tree': 79 - this.isViewTree = true; 80 - break; 81 - case 'tag': 82 - this.isViewTag = true; 83 - break; 84 - default: 85 - this.isViewBranch = true; 86 - break; 87 - } 88 - 89 - document.body.addEventListener('click', (event) => { 90 - if (elRoot.contains(event.target)) return; 91 - if (this.menuVisible) { 92 - this.menuVisible = false; 93 - } 94 - }); 95 - }, 96 - 97 - methods: { 98 - selectItem(item) { 99 - const prev = this.getSelected(); 100 - if (prev !== null) { 101 - prev.selected = false; 102 - } 103 - item.selected = true; 104 - const url = (item.tag) ? this.tagURLPrefix + item.url + this.tagURLSuffix : this.branchURLPrefix + item.url + this.branchURLSuffix; 105 - if (!this.branchForm) { 106 - window.location.href = url; 107 - } else { 108 - this.isViewTree = false; 109 - this.isViewTag = false; 110 - this.isViewBranch = false; 111 - this.$refs.dropdownRefName.textContent = item.name; 112 - if (this.setAction) { 113 - $(`#${this.branchForm}`).attr('action', url); 114 - } else { 115 - $(`#${this.branchForm} input[name="refURL"]`).val(url); 116 - } 117 - $(`#${this.branchForm} input[name="ref"]`).val(item.name); 118 - if (item.tag) { 119 - this.isViewTag = true; 120 - $(`#${this.branchForm} input[name="refType"]`).val('tag'); 121 - } else { 122 - this.isViewBranch = true; 123 - $(`#${this.branchForm} input[name="refType"]`).val('branch'); 124 - } 125 - if (this.submitForm) { 126 - $(`#${this.branchForm}`).trigger('submit'); 127 - } 128 - this.menuVisible = false; 129 - } 130 - }, 131 - createNewBranch() { 132 - if (!this.showCreateNewBranch) return; 133 - $(this.$refs.newBranchForm).trigger('submit'); 134 - }, 135 - focusSearchField() { 136 - nextTick(() => { 137 - this.$refs.searchField.focus(); 138 - }); 139 - }, 140 - getSelected() { 141 - for (let i = 0, j = this.items.length; i < j; ++i) { 142 - if (this.items[i].selected) return this.items[i]; 143 - } 144 - return null; 145 - }, 146 - getSelectedIndexInFiltered() { 147 - for (let i = 0, j = this.filteredItems.length; i < j; ++i) { 148 - if (this.filteredItems[i].selected) return i; 149 - } 150 - return -1; 151 - }, 152 - scrollToActive() { 153 - let el = this.$refs[`listItem${this.active}`]; 154 - if (!el || !el.length) return; 155 - if (Array.isArray(el)) { 156 - el = el[0]; 157 - } 158 - 159 - const cont = this.$refs.scrollContainer; 160 - if (el.offsetTop < cont.scrollTop) { 161 - cont.scrollTop = el.offsetTop; 162 - } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { 163 - cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; 164 - } 165 - }, 166 - keydown(event) { 167 - if (event.keyCode === 40) { // arrow down 168 - event.preventDefault(); 169 - 170 - if (this.active === -1) { 171 - this.active = this.getSelectedIndexInFiltered(); 172 - } 173 - 174 - if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { 175 - return; 176 - } 177 - this.active++; 178 - this.scrollToActive(); 179 - } else if (event.keyCode === 38) { // arrow up 180 - event.preventDefault(); 181 - 182 - if (this.active === -1) { 183 - this.active = this.getSelectedIndexInFiltered(); 184 - } 185 - 186 - if (this.active <= 0) { 187 - return; 188 - } 189 - this.active--; 190 - this.scrollToActive(); 191 - } else if (event.keyCode === 13) { // enter 192 - event.preventDefault(); 193 - 194 - if (this.active >= this.filteredItems.length) { 195 - this.createNewBranch(); 196 - } else if (this.active >= 0) { 197 - this.selectItem(this.filteredItems[this.active]); 198 - } 199 - } else if (event.keyCode === 27) { // escape 200 - event.preventDefault(); 201 - this.menuVisible = false; 202 - } 203 - } 204 - } 205 - }); 206 - view.mount(this); 207 - }); 208 - }
+293
web_src/js/components/RepoBranchTagSelector.vue
··· 1 + <template> 2 + <div class="ui floating filter dropdown custom"> 3 + <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> 4 + <span class="text gt-df gt-ac gt-mr-2"> 5 + <template v-if="release">{{ textReleaseCompare }}</template> 6 + <template v-else> 7 + <svg-icon v-if="isViewTag" name="octicon-tag" /> 8 + <svg-icon v-else name="octicon-git-branch"/> 9 + <strong ref="dropdownRefName" class="gt-ml-3">{{ refNameText }}</strong> 10 + </template> 11 + </span> 12 + <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/> 13 + </button> 14 + <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak> 15 + <div class="ui icon search input"> 16 + <i class="icon gt-df gt-ac gt-jc gt-m-0"><svg-icon name="octicon-filter" :size="16"/></i> 17 + <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder"> 18 + </div> 19 + <template v-if="showBranchesInDropdown"> 20 + <div class="header branch-tag-choice"> 21 + <div class="ui grid"> 22 + <div class="two column row"> 23 + <a class="reference column" href="#" @click="createTag = false; mode = 'branches'; focusSearchField()"> 24 + <span class="text" :class="{black: mode === 'branches'}"> 25 + <svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }} 26 + </span> 27 + </a> 28 + <template v-if="!noTag"> 29 + <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()"> 30 + <span class="text" :class="{black: mode === 'tags'}"> 31 + <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }} 32 + </span> 33 + </a> 34 + </template> 35 + </div> 36 + </div> 37 + </div> 38 + </template> 39 + <div class="scrolling menu" ref="scrollContainer"> 40 + <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"> 41 + {{ item.name }} 42 + </div> 43 + <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length"> 44 + <a href="#" @click="createNewBranch()"> 45 + <div v-show="createTag"> 46 + <i class="reference tags icon"/> 47 + <!-- eslint-disable-next-line vue/no-v-html --> 48 + <span v-html="textCreateTag.replace('%s', searchTerm)"/> 49 + </div> 50 + <div v-show="!createTag"> 51 + <svg-icon name="octicon-git-branch"/> 52 + <!-- eslint-disable-next-line vue/no-v-html --> 53 + <span v-html="textCreateBranch.replace('%s', searchTerm)"/> 54 + </div> 55 + <div class="text small"> 56 + <span v-if="isViewBranch || release">{{ textCreateBranchFrom.replace('%s', branchName) }}</span> 57 + <span v-else-if="isViewTag">{{ textCreateBranchFrom.replace('%s', tagName) }}</span> 58 + <span v-else>{{ textCreateBranchFrom.replace('%s', commitIdShort) }}</span> 59 + </div> 60 + </a> 61 + <form ref="newBranchForm" :action="formActionUrl" method="post"> 62 + <input type="hidden" name="_csrf" :value="csrfToken"> 63 + <input type="hidden" name="new_branch_name" v-model="searchTerm"> 64 + <input type="hidden" name="create_tag" v-model="createTag"> 65 + <input type="hidden" name="current_path" v-model="treePath" v-if="treePath"> 66 + </form> 67 + </div> 68 + </div> 69 + <div class="message" v-if="showNoResults"> 70 + {{ noResults }} 71 + </div> 72 + </div> 73 + </div> 74 + </template> 75 + 76 + <script> 77 + import {createApp, nextTick} from 'vue'; 78 + import $ from 'jquery'; 79 + import {SvgIcon} from '../svg.js'; 80 + import {pathEscapeSegments} from '../utils/url.js'; 81 + 82 + const sfc = { 83 + components: {SvgIcon}, 84 + 85 + // no `data()`, at the moment, the `data()` is provided by the init code, which is not ideal and should be fixed in the future 86 + 87 + computed: { 88 + filteredItems() { 89 + const items = this.items.filter((item) => { 90 + return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && 91 + (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); 92 + }); 93 + 94 + // TODO: fix this anti-pattern: side-effects-in-computed-properties 95 + this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); 96 + return items; 97 + }, 98 + showNoResults() { 99 + return this.filteredItems.length === 0 && !this.showCreateNewBranch; 100 + }, 101 + showCreateNewBranch() { 102 + if (this.disableCreateBranch || !this.searchTerm) { 103 + return false; 104 + } 105 + return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; 106 + }, 107 + formActionUrl() { 108 + return `${this.repoLink}/branches/_new/${pathEscapeSegments(this.branchNameSubURL)}`; 109 + }, 110 + }, 111 + 112 + watch: { 113 + menuVisible(visible) { 114 + if (visible) { 115 + this.focusSearchField(); 116 + } 117 + } 118 + }, 119 + 120 + beforeMount() { 121 + if (this.viewType === 'tree') { 122 + this.isViewTree = true; 123 + this.refNameText = this.commitIdShort; 124 + } else if (this.viewType === 'tag') { 125 + this.isViewTag = true; 126 + this.refNameText = this.tagName; 127 + } else { 128 + this.isViewBranch = true; 129 + this.refNameText = this.branchName; 130 + } 131 + 132 + document.body.addEventListener('click', (event) => { 133 + if (this.$el.contains(event.target)) return; 134 + if (this.menuVisible) { 135 + this.menuVisible = false; 136 + } 137 + }); 138 + }, 139 + 140 + methods: { 141 + selectItem(item) { 142 + const prev = this.getSelected(); 143 + if (prev !== null) { 144 + prev.selected = false; 145 + } 146 + item.selected = true; 147 + const url = (item.tag) ? this.tagURLPrefix + item.url + this.tagURLSuffix : this.branchURLPrefix + item.url + this.branchURLSuffix; 148 + if (!this.branchForm) { 149 + window.location.href = url; 150 + } else { 151 + this.isViewTree = false; 152 + this.isViewTag = false; 153 + this.isViewBranch = false; 154 + this.$refs.dropdownRefName.textContent = item.name; 155 + if (this.setAction) { 156 + $(`#${this.branchForm}`).attr('action', url); 157 + } else { 158 + $(`#${this.branchForm} input[name="refURL"]`).val(url); 159 + } 160 + $(`#${this.branchForm} input[name="ref"]`).val(item.name); 161 + if (item.tag) { 162 + this.isViewTag = true; 163 + $(`#${this.branchForm} input[name="refType"]`).val('tag'); 164 + } else { 165 + this.isViewBranch = true; 166 + $(`#${this.branchForm} input[name="refType"]`).val('branch'); 167 + } 168 + if (this.submitForm) { 169 + $(`#${this.branchForm}`).trigger('submit'); 170 + } 171 + this.menuVisible = false; 172 + } 173 + }, 174 + createNewBranch() { 175 + if (!this.showCreateNewBranch) return; 176 + $(this.$refs.newBranchForm).trigger('submit'); 177 + }, 178 + focusSearchField() { 179 + nextTick(() => { 180 + this.$refs.searchField.focus(); 181 + }); 182 + }, 183 + getSelected() { 184 + for (let i = 0, j = this.items.length; i < j; ++i) { 185 + if (this.items[i].selected) return this.items[i]; 186 + } 187 + return null; 188 + }, 189 + getSelectedIndexInFiltered() { 190 + for (let i = 0, j = this.filteredItems.length; i < j; ++i) { 191 + if (this.filteredItems[i].selected) return i; 192 + } 193 + return -1; 194 + }, 195 + scrollToActive() { 196 + let el = this.$refs[`listItem${this.active}`]; 197 + if (!el || !el.length) return; 198 + if (Array.isArray(el)) { 199 + el = el[0]; 200 + } 201 + 202 + const cont = this.$refs.scrollContainer; 203 + if (el.offsetTop < cont.scrollTop) { 204 + cont.scrollTop = el.offsetTop; 205 + } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { 206 + cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; 207 + } 208 + }, 209 + keydown(event) { 210 + if (event.keyCode === 40) { // arrow down 211 + event.preventDefault(); 212 + 213 + if (this.active === -1) { 214 + this.active = this.getSelectedIndexInFiltered(); 215 + } 216 + 217 + if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { 218 + return; 219 + } 220 + this.active++; 221 + this.scrollToActive(); 222 + } else if (event.keyCode === 38) { // arrow up 223 + event.preventDefault(); 224 + 225 + if (this.active === -1) { 226 + this.active = this.getSelectedIndexInFiltered(); 227 + } 228 + 229 + if (this.active <= 0) { 230 + return; 231 + } 232 + this.active--; 233 + this.scrollToActive(); 234 + } else if (event.keyCode === 13) { // enter 235 + event.preventDefault(); 236 + 237 + if (this.active >= this.filteredItems.length) { 238 + this.createNewBranch(); 239 + } else if (this.active >= 0) { 240 + this.selectItem(this.filteredItems[this.active]); 241 + } 242 + } else if (event.keyCode === 27) { // escape 243 + event.preventDefault(); 244 + this.menuVisible = false; 245 + } 246 + } 247 + } 248 + }; 249 + 250 + export function initRepoBranchTagSelector(selector) { 251 + for (const [elIndex, elRoot] of document.querySelectorAll(selector).entries()) { 252 + const data = { 253 + csrfToken: window.config.csrfToken, 254 + items: [], 255 + searchTerm: '', 256 + refNameText: '', 257 + menuVisible: false, 258 + createTag: false, 259 + release: null, 260 + 261 + isViewTag: false, 262 + isViewBranch: false, 263 + isViewTree: false, 264 + 265 + active: 0, 266 + 267 + ...window.config.pageData.branchDropdownDataList[elIndex], 268 + }; 269 + 270 + // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name" 271 + 272 + if (data.showBranchesInDropdown && data.branches) { 273 + for (const branch of data.branches) { 274 + data.items.push({name: branch, url: branch, branch: true, tag: false, selected: branch === data.defaultBranch}); 275 + } 276 + } 277 + if (!data.noTag && data.tags) { 278 + for (const tag of data.tags) { 279 + if (data.release) { 280 + data.items.push({name: tag, url: tag, branch: false, tag: true, selected: tag === data.release.tagName}); 281 + } else { 282 + data.items.push({name: tag, url: tag, branch: false, tag: true, selected: tag === data.defaultBranch}); 283 + } 284 + } 285 + } 286 + 287 + const comp = {...sfc, data() { return data }}; 288 + createApp(comp).mount(elRoot); 289 + } 290 + } 291 + 292 + export default sfc; // activate IDE's Vue plugin 293 + </script>
+2 -5
web_src/js/features/repo-findfile.js
··· 1 1 import $ from 'jquery'; 2 2 import {svg} from '../svg.js'; 3 3 import {toggleElem} from '../utils/dom.js'; 4 + import {pathEscapeSegments} from '../utils/url.js'; 4 5 5 6 const {csrf} = window.config; 6 7 ··· 73 74 return filterResult; 74 75 } 75 76 76 - export function escapePath(s) { 77 - return s.split('/').map(encodeURIComponent).join('/'); 78 - } 79 - 80 77 function filterRepoFiles(filter) { 81 78 const treeLink = $repoFindFileInput.attr('data-url-tree-link'); 82 79 $repoFindFileTableBody.empty(); ··· 88 85 for (const r of filterResult) { 89 86 const $row = $(tmplRow); 90 87 const $a = $row.find('a'); 91 - $a.attr('href', `${treeLink}/${escapePath(r.matchResult.join(''))}`); 88 + $a.attr('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); 92 89 const $octiconFile = $(svg('octicon-file')).addClass('gt-mr-3'); 93 90 $a.append($octiconFile); 94 91 // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
+1 -6
web_src/js/features/repo-findfile.test.js
··· 1 1 import {describe, expect, test} from 'vitest'; 2 - import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted, escapePath} from './repo-findfile.js'; 2 + import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted} from './repo-findfile.js'; 3 3 4 4 describe('Repo Find Files', () => { 5 5 test('strSubMatch', () => { ··· 31 31 res = filterRepoFilesWeighted(['we-got-result.dat', 'word.txt'], 'word'); 32 32 expect(res).toHaveLength(2); 33 33 expect(res[0].matchResult).toEqual(['', 'word', '.txt']); 34 - }); 35 - 36 - test('escapePath', () => { 37 - expect(escapePath('a/b/c')).toEqual('a/b/c'); 38 - expect(escapePath('a/b/ c')).toEqual('a/b/%20c'); 39 34 }); 40 35 });
+2 -2
web_src/js/features/repo-legacy.js
··· 11 11 import {initUnicodeEscapeButton} from './repo-unicode-escape.js'; 12 12 import {svg} from '../svg.js'; 13 13 import {htmlEscape} from 'escape-goat'; 14 - import {initRepoBranchTagDropdown} from '../components/RepoBranchTagDropdown.js'; 14 + import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; 15 15 import { 16 16 initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, 17 17 initRepoCommonLanguageStats, ··· 486 486 // File list and commits 487 487 if ($('.repository.file.list').length > 0 || $('.branch-dropdown').length > 0 || 488 488 $('.repository.commits').length > 0 || $('.repository.release').length > 0) { 489 - initRepoBranchTagDropdown('.choose.reference .ui.dropdown'); 489 + initRepoBranchTagSelector('.js-branch-tag-selector'); 490 490 } 491 491 492 492 // Wiki
+10 -8
web_src/js/svg.js
··· 1 + import {h} from 'vue'; 1 2 import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; 2 3 import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; 3 4 import octiconClock from '../../public/img/svg/octicon-clock.svg'; ··· 40 41 import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg'; 41 42 import octiconChevronLeft from '../../public/img/svg/octicon-chevron-left.svg'; 42 43 import octiconOrganization from '../../public/img/svg/octicon-organization.svg'; 44 + import octiconTag from '../../public/img/svg/octicon-tag.svg'; 45 + import octiconGitBranch from '../../public/img/svg/octicon-git-branch.svg'; 43 46 44 47 const svgs = { 45 48 'octicon-blocked': octiconBlocked, ··· 84 87 'gitea-double-chevron-right': giteaDoubleChevronRight, 85 88 'octicon-chevron-left': octiconChevronLeft, 86 89 'octicon-organization': octiconOrganization, 90 + 'octicon-tag': octiconTag, 91 + 'octicon-git-branch': octiconGitBranch, 87 92 }; 88 93 89 - // TODO: use a more general approach to access SVG icons. At the moment, developers must check, pick and fill the names manually, most of the SVG icons in assets couldn't be used directly. 94 + // TODO: use a more general approach to access SVG icons. 95 + // At the moment, developers must check, pick and fill the names manually, 96 + // most of the SVG icons in assets couldn't be used directly. 90 97 91 98 const parser = new DOMParser(); 92 99 const serializer = new XMLSerializer(); ··· 112 119 size: {type: Number, default: 16}, 113 120 className: {type: String, default: ''}, 114 121 }, 115 - 116 - computed: { 117 - svg() { 118 - return svg(this.name, this.size, this.className); 119 - }, 122 + render() { 123 + return h('span', {innerHTML: svg(this.name, this.size, this.className)}); 120 124 }, 121 - 122 - template: `<span v-html="svg" />` 123 125 };
+3
web_src/js/utils/url.js
··· 1 + export function pathEscapeSegments(s) { 2 + return s.split('/').map(encodeURIComponent).join('/'); 3 + }
+7
web_src/js/utils/url.test.js
··· 1 + import {expect, test} from 'vitest'; 2 + import {pathEscapeSegments} from './url.js'; 3 + 4 + test('pathEscapeSegments', () => { 5 + expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); 6 + expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c'); 7 + });
-4
web_src/less/_base.less
··· 1924 1924 display: block; 1925 1925 } 1926 1926 1927 - [v-cloak] { 1928 - display: none !important; 1929 - } 1930 - 1931 1927 .repos-search { 1932 1928 padding-bottom: 0 !important; 1933 1929 }
-6
web_src/less/_repository.less
··· 222 222 font-size: 1.2em; 223 223 } 224 224 225 - .choose.reference { 226 - .header .icon { 227 - font-size: 1.4em; 228 - } 229 - } 230 - 231 225 .repo-path { 232 226 233 227 .section,
+4
webpack.config.js
··· 196 196 ], 197 197 }, 198 198 plugins: [ 199 + new webpack.DefinePlugin({ 200 + __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API 201 + __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production 202 + }), 199 203 new VueLoaderPlugin(), 200 204 new MiniCssExtractPlugin({ 201 205 filename: 'css/[name].css',