aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--templates/repo/branch_dropdown.tmpl98
-rw-r--r--web_src/js/components/DashboardRepoList.vue2
-rw-r--r--web_src/js/components/PullRequestMergeForm.vue7
-rw-r--r--web_src/js/components/RepoBranchTagDropdown.js208
-rw-r--r--web_src/js/components/RepoBranchTagSelector.vue293
-rw-r--r--web_src/js/features/repo-findfile.js7
-rw-r--r--web_src/js/features/repo-findfile.test.js7
-rw-r--r--web_src/js/features/repo-legacy.js4
-rw-r--r--web_src/js/svg.js18
-rw-r--r--web_src/js/utils/url.js3
-rw-r--r--web_src/js/utils/url.test.js7
-rw-r--r--web_src/less/_base.less4
-rw-r--r--web_src/less/_repository.less6
-rw-r--r--webpack.config.js4
14 files changed, 359 insertions, 309 deletions
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index 8e81373aec..1ec4b7ef16 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}}
<script type="module">
@@ -30,8 +44,8 @@
'defaultBranch': {{$defaultBranch}},
'branchURLPrefix': '{{if .branchURLPrefix}}{{.branchURLPrefix}}{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/branch/{{end}}',
'branchURLSuffix': '{{if .branchURLSuffix}}{{.branchURLSuffix}}{{else}}{{if $.root.TreePath}}/{{PathEscapeSegments $.root.TreePath}}{{end}}{{end}}',
- 'tagURLPrefix': '{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if $release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}',
- '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}}',
+ 'tagURLPrefix': '{{if .tagURLPrefix}}{{.tagURLPrefix}}{{else if .release}}{{$.root.RepoLink}}/compare/{{else}}{{$.root.RepoLink}}/{{if $.root.PageIsCommits}}commits{{else}}src{{end}}/tag/{{end}}',
+ '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}}',
'repoLink': {{.root.RepoLink}},
'treePath': {{.root.TreePath}},
'branchNameSubURL': {{.root.BranchNameSubURL}},
@@ -46,71 +60,23 @@
window.config.pageData.branchDropdownDataList.push(data);
</script>
-<div class="fitted item choose reference">
+<div class="fitted item js-branch-tag-selector">
+ {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
<div class="ui floating filter dropdown custom">
- <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+ <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df">
<span class="text gt-df gt-ac gt-mr-2">
- {{/* v-cloak is used to hide unnecessary elements before Vue componment is mounted */}}
- <span v-cloak v-if="release">${ textReleaseCompare }</span>
- <span :class="{visible: isViewTag}" v-if="isViewTag" {{if not (eq $type "tag")}}v-cloak{{end}}>{{svg "octicon-tag"}}</span>
- <span :class="{visible: isViewBranch}" v-if="isViewBranch" {{if not (eq $type "branch")}}v-cloak{{end}}>{{svg "octicon-git-branch"}}</span>
- <span :class="{visible: isViewTree}" v-if="isViewTree" {{if not (eq $type "tree")}}v-cloak{{end}}>{{svg "octicon-git-branch"}}</span>
- <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>
+ {{if .release}}
+ {{.root.locale.Tr "repo.release.compare"}}
+ {{else}}
+ {{if eq $type "tag"}}
+ {{svg "octicon-tag"}}
+ {{else}}
+ {{svg "octicon-git-branch"}}
+ {{end}}
+ <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>
+ {{end}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
- <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
- <div class="ui icon search input">
- <i class="icon gt-df gt-ac gt-jc gt-m-0">{{svg "octicon-filter" 16}}</i>
- <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder">
- </div>
- <template v-if="showBranchesInDropdown">
- <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()">
- <span class="text" :class="{black: mode === 'branches'}">
- {{svg "octicon-git-branch" 16 "gt-mr-2"}}${ textBranches }
- </span>
- </a>
- <template v-if="!noTag">
- <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
- <span class="text" :class="{black: mode === 'tags'}">
- {{svg "octicon-tag" 16 "gt-mr-2"}}${ textTags }
- </span>
- </a>
- </template>
- </div>
- </div>
- </div>
- </template>
- <div class="scrolling menu" ref="scrollContainer">
- <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>
- <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
- <a href="#" @click="createNewBranch()">
- <div v-show="createTag">
- <i class="reference tags icon"></i>
- <span v-html="textCreateTag.replace('%s', searchTerm)"></span>
- </div>
- <div v-show="!createTag">
- {{svg "octicon-git-branch"}}
- <span v-html="textCreateBranch.replace('%s', searchTerm)"></span>
- </div>
- <div class="text small">
- <span v-if="isViewBranch || release">${ textCreateBranchFrom.replace('%s', branchName) }</span>
- <span v-else-if="isViewTag">${ textCreateBranchFrom.replace('%s', tagName) }</span>
- <span v-else>${ textCreateBranchFrom.replace('%s', commitIdShort) }</span>
- </div>
- </a>
- <form ref="newBranchForm" action="{{.root.RepoLink}}/branches/_new/{{.root.BranchNameSubURL}}" 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="current_path" v-model="treePath" v-if="treePath">
- </form>
- </div>
- </div>
- <div class="message" v-if="showNoResults">${ noResults }</div>
- </div>
</div>
</div>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index e295910fd0..cc76ab627f 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -73,7 +73,7 @@
<li v-for="repo in repos" :class="{'private': repo.private || repo.internal}" :key="repo.id">
<a class="repo-list-link gt-df gt-ac gt-sb" :href="repo.link">
<div class="item-name gt-df gt-ac gt-f1 gt-mr-2">
- <svg-icon :name="repoIcon(repo)" size="16" class-name="gt-mr-2"/>
+ <svg-icon :name="repoIcon(repo)" :size="16" class-name="gt-mr-2"/>
<div class="text gt-bold truncate gt-ml-1">{{ repo.full_name }}</div>
<span v-if="repo.archived">
<svg-icon name="octicon-archive" :size="16" class-name="gt-ml-2"/>
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index fc610d2194..4d8c14a76d 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"}'
-->
<div>
- <!-- eslint-disable -->
- <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"></div>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/>
<div class="ui form" v-if="showActionForm">
<form :action="mergeForm.baseLink+'/merge'" method="post">
@@ -30,7 +30,8 @@
<button @click.prevent="clearMergeMessage" class="ui tertiary button">
{{ mergeForm.textClearMergeMessage }}
</button>
- <div class="ui label"><!-- TODO: Convert to tooltip once we can use tooltips in Vue templates -->
+ <div class="ui label">
+ <!-- TODO: Convert to tooltip once we can use tooltips in Vue templates -->
{{ mergeForm.textClearMergeMessageHint }}
</div>
</template>
diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js
deleted file mode 100644
index a8945b82d1..0000000000
--- 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 0000000000..6a65eeec6f
--- /dev/null
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -0,0 +1,293 @@
+<template>
+ <div class="ui floating filter dropdown custom">
+ <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
+ <span class="text gt-df gt-ac gt-mr-2">
+ <template v-if="release">{{ textReleaseCompare }}</template>
+ <template v-else>
+ <svg-icon v-if="isViewTag" name="octicon-tag" />
+ <svg-icon v-else name="octicon-git-branch"/>
+ <strong ref="dropdownRefName" class="gt-ml-3">{{ refNameText }}</strong>
+ </template>
+ </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="ui icon search input">
+ <i class="icon gt-df gt-ac gt-jc gt-m-0"><svg-icon name="octicon-filter" :size="16"/></i>
+ <input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder">
+ </div>
+ <template v-if="showBranchesInDropdown">
+ <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()">
+ <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()">
+ <span class="text" :class="{black: mode === 'tags'}">
+ <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }}
+ </span>
+ </a>
+ </template>
+ </div>
+ </div>
+ </div>
+ </template>
+ <div class="scrolling menu" ref="scrollContainer">
+ <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>
+ <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
+ <a href="#" @click="createNewBranch()">
+ <div v-show="createTag">
+ <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">
+ <svg-icon name="octicon-git-branch"/>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <span v-html="textCreateBranch.replace('%s', searchTerm)"/>
+ </div>
+ <div class="text small">
+ <span v-if="isViewBranch || release">{{ textCreateBranchFrom.replace('%s', branchName) }}</span>
+ <span v-else-if="isViewTag">{{ textCreateBranchFrom.replace('%s', tagName) }}</span>
+ <span v-else>{{ textCreateBranchFrom.replace('%s', commitIdShort) }}</span>
+ </div>
+ </a>
+ <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="current_path" v-model="treePath" v-if="treePath">
+ </form>
+ </div>
+ </div>
+ <div class="message" v-if="showNoResults">
+ {{ noResults }}
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import {createApp, nextTick} from 'vue';
+import $ from 'jquery';
+import {SvgIcon} from '../svg.js';
+import {pathEscapeSegments} from '../utils/url.js';
+
+const sfc = {
+ components: {SvgIcon},
+
+ // no `data()`, at the moment, the `data()` is provided by the init code, which is not ideal and should be fixed in the future
+
+ 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()));
+ });
+
+ // TODO: fix this anti-pattern: side-effects-in-computed-properties
+ this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1);
+ 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;
+ },
+ formActionUrl() {
+ return `${this.repoLink}/branches/_new/${pathEscapeSegments(this.branchNameSubURL)}`;
+ },
+ },
+
+ watch: {
+ menuVisible(visible) {
+ if (visible) {
+ this.focusSearchField();
+ }
+ }
+ },
+
+ beforeMount() {
+ if (this.viewType === 'tree') {
+ this.isViewTree = true;
+ this.refNameText = this.commitIdShort;
+ } else if (this.viewType === 'tag') {
+ this.isViewTag = true;
+ this.refNameText = this.tagName;
+ } else {
+ this.isViewBranch = true;
+ this.refNameText = this.branchName;
+ }
+
+ document.body.addEventListener('click', (event) => {
+ if (this.$el.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;
+ }
+ }
+ }
+};
+
+export function initRepoBranchTagSelector(selector) {
+ for (const [elIndex, elRoot] of document.querySelectorAll(selector).entries()) {
+ const data = {
+ csrfToken: window.config.csrfToken,
+ items: [],
+ searchTerm: '',
+ refNameText: '',
+ menuVisible: false,
+ createTag: false,
+ release: null,
+
+ isViewTag: false,
+ isViewBranch: false,
+ isViewTree: false,
+
+ active: 0,
+
+ ...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: 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 comp = {...sfc, data() { return data }};
+ createApp(comp).mount(elRoot);
+ }
+}
+
+export default sfc; // activate IDE's Vue plugin
+</script>
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index 093f90fe8e..078c822aa2 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 5032185396..a90b0bf0a2 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 5346a0d274..4454b92ccc 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 9eabca3fd3..e431ca57e6 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: `<span v-html="svg" />`
};
diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js
new file mode 100644
index 0000000000..a40737ca6f
--- /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 0000000000..ef2ffaa5f9
--- /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 1cf65e784c..cabf707aad 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 0069a31cec..c842c4ca65 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 245791e7ea..46bdd6acfa 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',