diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 8e81373ae..1ec4b7ef1 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -1,6 +1,20 @@ -{{$release := .release}} -{{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}} -{{$type := ""}}{{if and .root.IsViewTag (not .noTag)}}{{$type = "tag"}}{{else if .root.IsViewBranch}}{{$type = "branch"}}{{else}}{{$type = "tree"}}{{end}} +{{$defaultBranch := $.root.BranchName}} +{{if and .root.IsViewTag (not .noTag)}} + {{$defaultBranch = .root.TagName}} +{{end}} +{{if eq $defaultBranch ""}} + {{$defaultBranch = $.root.Repository.DefaultBranch}} +{{end}} + +{{$type := ""}} +{{if and .root.IsViewTag (not .noTag)}} + {{$type = "tag"}} +{{else if .root.IsViewBranch}} + {{$type = "branch"}} +{{else}} + {{$type = "tree"}} +{{end}} + {{$showBranchesInDropdown := not .root.HideBranchesInDropdown}} -
+
+ {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index e295910fd..cc76ab627 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -73,7 +73,7 @@
  • - +
    {{ repo.full_name }}
    diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index fc610d219..4d8c14a76 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -10,8 +10,8 @@ -d '{"context": "test/context", "description": "description", "state": "${state}", "target_url": "http://localhost"}' -->
    - -
    + +
    @@ -30,7 +30,8 @@ -
    +
    + {{ mergeForm.textClearMergeMessageHint }}
    diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js deleted file mode 100644 index a8945b82d..000000000 --- a/web_src/js/components/RepoBranchTagDropdown.js +++ /dev/null @@ -1,208 +0,0 @@ -import {createApp, nextTick} from 'vue'; -import $ from 'jquery'; - -export function initRepoBranchTagDropdown(selector) { - $(selector).each(function (dropdownIndex, elRoot) { - const data = { - csrfToken: window.config.csrfToken, - items: [], - searchTerm: '', - menuVisible: false, - createTag: false, - release: null, - - isViewTag: false, - isViewBranch: false, - isViewTree: false, - - active: 0, - - ...window.config.pageData.branchDropdownDataList[dropdownIndex], - }; - - // 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: 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: tag, branch: false, tag: true, selected: tag === data.release.tagName}); - } else { - data.items.push({name: tag, url: tag, branch: false, tag: true, selected: tag === data.defaultBranch}); - } - } - } - - const view = createApp({ - delimiters: ['${', '}'], - data() { - return data; - }, - computed: { - filteredItems() { - const items = this.items.filter((item) => { - return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && - (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); - }); - - // no idea how to fix this so linting rule is disabled instead - this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties - return items; - }, - showNoResults() { - return this.filteredItems.length === 0 && !this.showCreateNewBranch; - }, - showCreateNewBranch() { - if (this.disableCreateBranch || !this.searchTerm) { - return false; - } - - return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; - } - }, - - watch: { - menuVisible(visible) { - if (visible) { - this.focusSearchField(); - } - } - }, - - beforeMount() { - switch (data.viewType) { - case 'tree': - this.isViewTree = true; - break; - case 'tag': - this.isViewTag = true; - break; - default: - this.isViewBranch = true; - break; - } - - document.body.addEventListener('click', (event) => { - if (elRoot.contains(event.target)) return; - if (this.menuVisible) { - this.menuVisible = false; - } - }); - }, - - methods: { - selectItem(item) { - const prev = this.getSelected(); - if (prev !== null) { - prev.selected = false; - } - item.selected = true; - const url = (item.tag) ? this.tagURLPrefix + item.url + this.tagURLSuffix : this.branchURLPrefix + item.url + this.branchURLSuffix; - if (!this.branchForm) { - window.location.href = url; - } else { - this.isViewTree = false; - this.isViewTag = false; - this.isViewBranch = false; - this.$refs.dropdownRefName.textContent = item.name; - if (this.setAction) { - $(`#${this.branchForm}`).attr('action', url); - } else { - $(`#${this.branchForm} input[name="refURL"]`).val(url); - } - $(`#${this.branchForm} input[name="ref"]`).val(item.name); - if (item.tag) { - this.isViewTag = true; - $(`#${this.branchForm} input[name="refType"]`).val('tag'); - } else { - this.isViewBranch = true; - $(`#${this.branchForm} input[name="refType"]`).val('branch'); - } - if (this.submitForm) { - $(`#${this.branchForm}`).trigger('submit'); - } - this.menuVisible = false; - } - }, - createNewBranch() { - if (!this.showCreateNewBranch) return; - $(this.$refs.newBranchForm).trigger('submit'); - }, - focusSearchField() { - nextTick(() => { - this.$refs.searchField.focus(); - }); - }, - getSelected() { - for (let i = 0, j = this.items.length; i < j; ++i) { - if (this.items[i].selected) return this.items[i]; - } - return null; - }, - getSelectedIndexInFiltered() { - for (let i = 0, j = this.filteredItems.length; i < j; ++i) { - if (this.filteredItems[i].selected) return i; - } - return -1; - }, - scrollToActive() { - let el = this.$refs[`listItem${this.active}`]; - if (!el || !el.length) return; - if (Array.isArray(el)) { - el = el[0]; - } - - const cont = this.$refs.scrollContainer; - if (el.offsetTop < cont.scrollTop) { - cont.scrollTop = el.offsetTop; - } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { - cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; - } - }, - keydown(event) { - if (event.keyCode === 40) { // arrow down - event.preventDefault(); - - if (this.active === -1) { - this.active = this.getSelectedIndexInFiltered(); - } - - if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { - return; - } - this.active++; - this.scrollToActive(); - } else if (event.keyCode === 38) { // arrow up - event.preventDefault(); - - if (this.active === -1) { - this.active = this.getSelectedIndexInFiltered(); - } - - if (this.active <= 0) { - return; - } - this.active--; - this.scrollToActive(); - } else if (event.keyCode === 13) { // enter - event.preventDefault(); - - if (this.active >= this.filteredItems.length) { - this.createNewBranch(); - } else if (this.active >= 0) { - this.selectItem(this.filteredItems[this.active]); - } - } else if (event.keyCode === 27) { // escape - event.preventDefault(); - this.menuVisible = false; - } - } - } - }); - view.mount(this); - }); -} diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue new file mode 100644 index 000000000..6a65eeec6 --- /dev/null +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -0,0 +1,293 @@ + + + diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js index 093f90fe8..078c822aa 100644 --- a/web_src/js/features/repo-findfile.js +++ b/web_src/js/features/repo-findfile.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import {svg} from '../svg.js'; import {toggleElem} from '../utils/dom.js'; +import {pathEscapeSegments} from '../utils/url.js'; const {csrf} = window.config; @@ -73,10 +74,6 @@ export function filterRepoFilesWeighted(files, filter) { return filterResult; } -export function escapePath(s) { - return s.split('/').map(encodeURIComponent).join('/'); -} - function filterRepoFiles(filter) { const treeLink = $repoFindFileInput.attr('data-url-tree-link'); $repoFindFileTableBody.empty(); @@ -88,7 +85,7 @@ function filterRepoFiles(filter) { for (const r of filterResult) { const $row = $(tmplRow); const $a = $row.find('a'); - $a.attr('href', `${treeLink}/${escapePath(r.matchResult.join(''))}`); + $a.attr('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); const $octiconFile = $(svg('octicon-file')).addClass('gt-mr-3'); $a.append($octiconFile); // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] diff --git a/web_src/js/features/repo-findfile.test.js b/web_src/js/features/repo-findfile.test.js index 503218539..a90b0bf0a 100644 --- a/web_src/js/features/repo-findfile.test.js +++ b/web_src/js/features/repo-findfile.test.js @@ -1,5 +1,5 @@ import {describe, expect, test} from 'vitest'; -import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted, escapePath} from './repo-findfile.js'; +import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted} from './repo-findfile.js'; describe('Repo Find Files', () => { test('strSubMatch', () => { @@ -32,9 +32,4 @@ describe('Repo Find Files', () => { expect(res).toHaveLength(2); expect(res[0].matchResult).toEqual(['', 'word', '.txt']); }); - - test('escapePath', () => { - expect(escapePath('a/b/c')).toEqual('a/b/c'); - expect(escapePath('a/b/ c')).toEqual('a/b/%20c'); - }); }); diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 5346a0d27..4454b92cc 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -11,7 +11,7 @@ import { import {initUnicodeEscapeButton} from './repo-unicode-escape.js'; import {svg} from '../svg.js'; import {htmlEscape} from 'escape-goat'; -import {initRepoBranchTagDropdown} from '../components/RepoBranchTagDropdown.js'; +import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; import { initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, initRepoCommonLanguageStats, @@ -486,7 +486,7 @@ export function initRepository() { // File list and commits if ($('.repository.file.list').length > 0 || $('.branch-dropdown').length > 0 || $('.repository.commits').length > 0 || $('.repository.release').length > 0) { - initRepoBranchTagDropdown('.choose.reference .ui.dropdown'); + initRepoBranchTagSelector('.js-branch-tag-selector'); } // Wiki diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 9eabca3fd..e431ca57e 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -1,3 +1,4 @@ +import {h} from 'vue'; import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; import octiconClock from '../../public/img/svg/octicon-clock.svg'; @@ -40,6 +41,8 @@ import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-le import giteaDoubleChevronRight from '../../public/img/svg/gitea-double-chevron-right.svg'; import octiconChevronLeft from '../../public/img/svg/octicon-chevron-left.svg'; import octiconOrganization from '../../public/img/svg/octicon-organization.svg'; +import octiconTag from '../../public/img/svg/octicon-tag.svg'; +import octiconGitBranch from '../../public/img/svg/octicon-git-branch.svg'; const svgs = { 'octicon-blocked': octiconBlocked, @@ -84,9 +87,13 @@ const svgs = { 'gitea-double-chevron-right': giteaDoubleChevronRight, 'octicon-chevron-left': octiconChevronLeft, 'octicon-organization': octiconOrganization, + 'octicon-tag': octiconTag, + 'octicon-git-branch': octiconGitBranch, }; -// 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. +// 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. const parser = new DOMParser(); const serializer = new XMLSerializer(); @@ -112,12 +119,7 @@ export const SvgIcon = { size: {type: Number, default: 16}, className: {type: String, default: ''}, }, - - computed: { - svg() { - return svg(this.name, this.size, this.className); - }, + render() { + return h('span', {innerHTML: svg(this.name, this.size, this.className)}); }, - - template: `` }; diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js new file mode 100644 index 000000000..a40737ca6 --- /dev/null +++ b/web_src/js/utils/url.js @@ -0,0 +1,3 @@ +export function pathEscapeSegments(s) { + return s.split('/').map(encodeURIComponent).join('/'); +} diff --git a/web_src/js/utils/url.test.js b/web_src/js/utils/url.test.js new file mode 100644 index 000000000..ef2ffaa5f --- /dev/null +++ b/web_src/js/utils/url.test.js @@ -0,0 +1,7 @@ +import {expect, test} from 'vitest'; +import {pathEscapeSegments} from './url.js'; + +test('pathEscapeSegments', () => { + expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); + expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c'); +}); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 1cf65e784..cabf707aa 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -1924,10 +1924,6 @@ footer { display: block; } -[v-cloak] { - display: none !important; -} - .repos-search { padding-bottom: 0 !important; } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 0069a31ce..c842c4ca6 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -222,12 +222,6 @@ font-size: 1.2em; } - .choose.reference { - .header .icon { - font-size: 1.4em; - } - } - .repo-path { .section, diff --git a/webpack.config.js b/webpack.config.js index 245791e7e..46bdd6acf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -196,6 +196,10 @@ export default { ], }, plugins: [ + new webpack.DefinePlugin({ + __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API + __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production + }), new VueLoaderPlugin(), new MiniCssExtractPlugin({ filename: 'css/[name].css',