1
0
Fork 0
forked from forgejo/forgejo

Frontend refactor: move Vue related code from index.js to components dir, and remove unused codes. (#17301)

* frontend refactor

* Apply suggestions from code review

Co-authored-by: delvh <dev.lh@web.de>

* Update templates/base/head.tmpl

Co-authored-by: delvh <dev.lh@web.de>

* Update docs/content/doc/developers/guidelines-frontend.md

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

* fix typo

* fix typo

* refactor PageData to pageData

* Apply suggestions from code review

Co-authored-by: delvh <dev.lh@web.de>

* Simply for the visual difference.

Co-authored-by: delvh <dev.lh@web.de>

* Revert "Apply suggestions from code review"

This reverts commit 4d78ad9b0e.

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
wxiaoguang 2021-10-15 10:35:26 +08:00 committed by GitHub
parent 96ff3e310f
commit 56362043d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 718 additions and 634 deletions

View file

@ -70,4 +70,3 @@ export default {
},
};
</script>
<style scoped/>

View file

@ -0,0 +1,370 @@
import Vue from 'vue';
import {initVueSvg, vueDelimiters} from './VueComponentLoader.js';
const {AppSubUrl, AssetUrlPrefix, pageData} = window.config;
function initVueComponents() {
Vue.component('repo-search', {
delimiters: vueDelimiters,
props: {
searchLimit: {
type: Number,
default: 10
},
subUrl: {
type: String,
required: true
},
uid: {
type: Number,
default: 0
},
teamId: {
type: Number,
required: false,
default: 0
},
organizations: {
type: Array,
default: () => [],
},
isOrganization: {
type: Boolean,
default: true
},
canCreateOrganization: {
type: Boolean,
default: false
},
organizationsTotalCount: {
type: Number,
default: 0
},
moreReposLink: {
type: String,
default: ''
}
},
data() {
const params = new URLSearchParams(window.location.search);
let tab = params.get('repo-search-tab');
if (!tab) {
tab = 'repos';
}
let reposFilter = params.get('repo-search-filter');
if (!reposFilter) {
reposFilter = 'all';
}
let privateFilter = params.get('repo-search-private');
if (!privateFilter) {
privateFilter = 'both';
}
let archivedFilter = params.get('repo-search-archived');
if (!archivedFilter) {
archivedFilter = 'unarchived';
}
let searchQuery = params.get('repo-search-query');
if (!searchQuery) {
searchQuery = '';
}
let page = 1;
try {
page = parseInt(params.get('repo-search-page'));
} catch {
// noop
}
if (!page) {
page = 1;
}
return {
tab,
repos: [],
reposTotalCount: 0,
reposFilter,
archivedFilter,
privateFilter,
page,
finalPage: 1,
searchQuery,
isLoading: false,
staticPrefix: AssetUrlPrefix,
counts: {},
repoTypes: {
all: {
searchMode: '',
},
forks: {
searchMode: 'fork',
},
mirrors: {
searchMode: 'mirror',
},
sources: {
searchMode: 'source',
},
collaborative: {
searchMode: 'collaborative',
},
}
};
},
computed: {
// used in `repolist.tmpl`
showMoreReposLink() {
return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
},
searchURL() {
return `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
}&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
}${this.reposFilter !== 'all' ? '&exclusive=1' : ''
}${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
}${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
}`;
},
repoTypeCount() {
return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
}
},
mounted() {
this.changeReposFilter(this.reposFilter);
$(this.$el).find('.poping.up').popup();
$(this.$el).find('.dropdown').dropdown();
this.setCheckboxes();
Vue.nextTick(() => {
this.$refs.search.focus();
});
},
methods: {
changeTab(t) {
this.tab = t;
this.updateHistory();
},
setCheckboxes() {
switch (this.archivedFilter) {
case 'unarchived':
$('#archivedFilterCheckbox').checkbox('set unchecked');
break;
case 'archived':
$('#archivedFilterCheckbox').checkbox('set checked');
break;
case 'both':
$('#archivedFilterCheckbox').checkbox('set indeterminate');
break;
default:
this.archivedFilter = 'unarchived';
$('#archivedFilterCheckbox').checkbox('set unchecked');
break;
}
switch (this.privateFilter) {
case 'public':
$('#privateFilterCheckbox').checkbox('set unchecked');
break;
case 'private':
$('#privateFilterCheckbox').checkbox('set checked');
break;
case 'both':
$('#privateFilterCheckbox').checkbox('set indeterminate');
break;
default:
this.privateFilter = 'both';
$('#privateFilterCheckbox').checkbox('set indeterminate');
break;
}
},
changeReposFilter(filter) {
this.reposFilter = filter;
this.repos = [];
this.page = 1;
Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
updateHistory() {
const params = new URLSearchParams(window.location.search);
if (this.tab === 'repos') {
params.delete('repo-search-tab');
} else {
params.set('repo-search-tab', this.tab);
}
if (this.reposFilter === 'all') {
params.delete('repo-search-filter');
} else {
params.set('repo-search-filter', this.reposFilter);
}
if (this.privateFilter === 'both') {
params.delete('repo-search-private');
} else {
params.set('repo-search-private', this.privateFilter);
}
if (this.archivedFilter === 'unarchived') {
params.delete('repo-search-archived');
} else {
params.set('repo-search-archived', this.archivedFilter);
}
if (this.searchQuery === '') {
params.delete('repo-search-query');
} else {
params.set('repo-search-query', this.searchQuery);
}
if (this.page === 1) {
params.delete('repo-search-page');
} else {
params.set('repo-search-page', `${this.page}`);
}
const queryString = params.toString();
if (queryString) {
window.history.replaceState({}, '', `?${queryString}`);
} else {
window.history.replaceState({}, '', window.location.pathname);
}
},
toggleArchivedFilter() {
switch (this.archivedFilter) {
case 'both':
this.archivedFilter = 'unarchived';
break;
case 'unarchived':
this.archivedFilter = 'archived';
break;
case 'archived':
this.archivedFilter = 'both';
break;
default:
this.archivedFilter = 'unarchived';
break;
}
this.page = 1;
this.repos = [];
this.setCheckboxes();
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
togglePrivateFilter() {
switch (this.privateFilter) {
case 'both':
this.privateFilter = 'public';
break;
case 'public':
this.privateFilter = 'private';
break;
case 'private':
this.privateFilter = 'both';
break;
default:
this.privateFilter = 'both';
break;
}
this.page = 1;
this.repos = [];
this.setCheckboxes();
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
changePage(page) {
this.page = page;
if (this.page > this.finalPage) {
this.page = this.finalPage;
}
if (this.page < 1) {
this.page = 1;
}
this.repos = [];
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
searchRepos() {
this.isLoading = true;
if (!this.reposTotalCount) {
const totalCountSearchURL = `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
$.getJSON(totalCountSearchURL, (_result, _textStatus, request) => {
this.reposTotalCount = request.getResponseHeader('X-Total-Count');
});
}
const searchedMode = this.repoTypes[this.reposFilter].searchMode;
const searchedURL = this.searchURL;
const searchedQuery = this.searchQuery;
$.getJSON(searchedURL, (result, _textStatus, request) => {
if (searchedURL === this.searchURL) {
this.repos = result.data;
const count = request.getResponseHeader('X-Total-Count');
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
this.reposTotalCount = count;
}
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count);
this.finalPage = Math.ceil(count / this.searchLimit);
this.updateHistory();
}
}).always(() => {
if (searchedURL === this.searchURL) {
this.isLoading = false;
}
});
},
repoIcon(repo) {
if (repo.fork) {
return 'octicon-repo-forked';
} else if (repo.mirror) {
return 'octicon-mirror';
} else if (repo.template) {
return `octicon-repo-template`;
} else if (repo.private) {
return 'octicon-lock';
} else if (repo.internal) {
return 'octicon-repo';
}
return 'octicon-repo';
}
}
});
}
function initDashboardRepoList() {
const el = document.getElementById('dashboard-repo-list');
const dashboardRepoListData = pageData.dashboardRepoList || null;
if (!el || !dashboardRepoListData) return;
initVueSvg();
initVueComponents();
new Vue({
el,
delimiters: vueDelimiters,
data: () => {
return {
searchLimit: dashboardRepoListData.searchLimit || 0,
subUrl: AppSubUrl,
};
},
});
}
export {initDashboardRepoList};

View file

@ -1,9 +1,9 @@
<template>
<div>
<div class="activity-bar-graph" ref="style" style="width:0px;height:0px"/>
<div class="activity-bar-graph-alt" ref="altStyle" style="width:0px;height:0px"/>
<div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/>
<div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/>
<vue-bar-graph
:points="graphData"
:points="graphPoints"
:show-x-axis="true"
:show-y-axis="false"
:show-values="true"
@ -15,9 +15,9 @@
:label-height="20"
>
<template #label="opt">
<g v-for="(author, idx) in authors" :key="author.position">
<g v-for="(author, idx) in graphAuthors" :key="author.position">
<a
v-if="opt.bar.index === idx && author.home_link !== ''"
v-if="opt.bar.index === idx && author.home_link"
:href="author.home_link"
>
<image
@ -39,7 +39,7 @@
</g>
</template>
<template #title="opt">
<tspan v-for="(author, idx) in authors" :key="author.position">
<tspan v-for="(author, idx) in graphAuthors" :key="author.position">
<tspan v-if="opt.bar.index === idx">
{{ author.name }}
</tspan>
@ -48,32 +48,39 @@
</vue-bar-graph>
</div>
</template>
<script>
import VueBarGraph from 'vue-bar-graph';
import {initVueApp} from './VueComponentLoader.js';
export default {
const sfc = {
components: {VueBarGraph},
props: {
data: {type: Array, default: () => []},
},
data: () => ({
colors: {
barColor: 'green',
textColor: 'black',
textAltColor: 'white',
},
// possible keys:
// * avatar_link: (...)
// * commits: (...)
// * home_link: (...)
// * login: (...)
// * name: (...)
activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [],
}),
computed: {
graphData() {
return this.data.map((item) => {
graphPoints() {
return this.activityTopAuthors.map((item) => {
return {
value: item.commits,
label: item.name,
};
});
},
authors() {
return this.data.map((item, idx) => {
graphAuthors() {
return this.activityTopAuthors.map((item, idx) => {
return {
position: idx + 1,
...item,
@ -81,21 +88,23 @@ export default {
});
},
graphWidth() {
return this.data.length * 40;
return this.activityTopAuthors.length * 40;
},
},
mounted() {
const st = window.getComputedStyle(this.$refs.style);
const stalt = window.getComputedStyle(this.$refs.altStyle);
const refStyle = window.getComputedStyle(this.$refs.style);
const refAltStyle = window.getComputedStyle(this.$refs.altStyle);
this.colors.barColor = st.backgroundColor;
this.colors.textColor = st.color;
this.colors.textAltColor = stalt.color;
},
methods: {
hasHomeLink(i) {
return this.graphData[i].homeLink !== '' && this.graphData[i].homeLink !== null;
},
this.colors.barColor = refStyle.backgroundColor;
this.colors.textColor = refStyle.color;
this.colors.textAltColor = refAltStyle.color;
}
};
function initRepoActivityTopAuthorsChart() {
initVueApp('#repo-activity-top-authors-chart', sfc);
}
export default sfc;
export {initRepoActivityTopAuthorsChart};
</script>

View file

@ -0,0 +1,161 @@
import Vue from 'vue';
function initRepoBranchTagDropdown(selector) {
$(selector).each(function () {
const $dropdown = $(this);
const $data = $dropdown.find('.data');
const data = {
items: [],
mode: $data.data('mode'),
searchTerm: '',
noResults: '',
canCreateBranch: false,
menuVisible: false,
createTag: false,
active: 0
};
$data.find('.item').each(function () {
data.items.push({
name: $(this).text(),
url: $(this).data('url'),
branch: $(this).hasClass('branch'),
tag: $(this).hasClass('tag'),
selected: $(this).hasClass('selected')
});
});
$data.remove();
new Vue({
el: this,
delimiters: ['${', '}'],
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.canCreateBranch || !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() {
this.noResults = this.$el.getAttribute('data-no-results');
this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true';
document.body.addEventListener('click', (event) => {
if (this.$el.contains(event.target)) return;
if (this.menuVisible) {
Vue.set(this, 'menuVisible', false);
}
});
},
methods: {
selectItem(item) {
const prev = this.getSelected();
if (prev !== null) {
prev.selected = false;
}
item.selected = true;
window.location.href = item.url;
},
createNewBranch() {
if (!this.showCreateNewBranch) return;
$(this.$refs.newBranchForm).trigger('submit');
},
focusSearchField() {
Vue.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 {initRepoBranchTagDropdown};

View file

@ -0,0 +1,52 @@
import Vue from 'vue';
import {svgs} from '../svg.js';
const vueDelimiters = ['${', '}'];
let vueEnvInited = false;
function initVueEnv() {
if (vueEnvInited) return;
vueEnvInited = true;
const isProd = window.config.IsProd;
Vue.config.productionTip = false;
Vue.config.devtools = !isProd;
}
let vueSvgInited = false;
function initVueSvg() {
if (vueSvgInited) return;
vueSvgInited = true;
// register svg icon vue components, e.g. <octicon-repo size="16"/>
for (const [name, htmlString] of Object.entries(svgs)) {
const template = htmlString
.replace(/height="[0-9]+"/, 'v-bind:height="size"')
.replace(/width="[0-9]+"/, 'v-bind:width="size"');
Vue.component(name, {
props: {
size: {
type: String,
default: '16',
},
},
template,
});
}
}
function initVueApp(el, opts = {}) {
if (typeof el === 'string') {
el = document.querySelector(el);
}
if (!el) return null;
return new Vue(Object.assign({
el,
delimiters: vueDelimiters,
}, opts));
}
export {vueDelimiters, initVueEnv, initVueSvg, initVueApp};

View file

@ -1,5 +1,5 @@
export function initAdminUserListSearchForm() {
const searchForm = window.config.PageData.adminUserListSearchForm;
const searchForm = window.config.pageData.adminUserListSearchForm;
if (!searchForm) return;
const $form = $('#user-list-search-form');

View file

@ -1,10 +1,13 @@
import './publicpath.js';
import Vue from 'vue';
import {htmlEscape} from 'escape-goat';
import 'jquery.are-you-sure';
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
import {initVueEnv} from './components/VueComponentLoader.js';
import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
import {initDashboardRepoList} from './components/DashboardRepoList.js';
import {initRepoBranchTagDropdown} from './components/RepoBranchTagDropdown.js';
import attachTribute from './features/tribute.js';
import createColorPicker from './features/colorpicker.js';
import createDropzone from './features/dropzone.js';
@ -27,20 +30,16 @@ import {initStopwatch} from './features/stopwatch.js';
import {showLineButton} from './code/linebutton.js';
import {initMarkupContent, initCommentContent} from './markup/content.js';
import {stripTags, mqBinarySearch} from './utils.js';
import {svg, svgs} from './svg.js';
import {svg} from './svg.js';
const {AppSubUrl, AssetUrlPrefix, csrf} = window.config;
const {AppSubUrl, csrf} = window.config;
let previewFileModes;
const commentMDEditors = {};
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
// Silence Vue's console advertisements in dev mode
// To use the Vue browser extension, enable the devtools option temporarily
Vue.config.productionTip = false;
Vue.config.devtools = false;
initVueEnv();
function initCommentPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu');
@ -806,7 +805,7 @@ async function initRepository() {
// File list and commits
if ($('.repository.file.list').length > 0 ||
$('.repository.commits').length > 0 || $('.repository.release').length > 0) {
initFilterBranchTagDropdown('.choose.reference .dropdown');
initRepoBranchTagDropdown('.choose.reference .dropdown');
}
// Wiki
@ -2858,7 +2857,8 @@ $(document).ready(async () => {
initWebhook();
initAdmin();
initCodeView();
initVueApp();
initRepoActivityTopAuthorsChart();
initDashboardRepoList();
initTeamSettings();
initCtrlEnterSubmit();
initNavbarContentToggle();
@ -3105,369 +3105,6 @@ function linkEmailAction(e) {
e.preventDefault();
}
function initVueComponents() {
// register svg icon vue components, e.g. <octicon-repo size="16"/>
for (const [name, htmlString] of Object.entries(svgs)) {
const template = htmlString
.replace(/height="[0-9]+"/, 'v-bind:height="size"')
.replace(/width="[0-9]+"/, 'v-bind:width="size"');
Vue.component(name, {
props: {
size: {
type: String,
default: '16',
},
},
template,
});
}
const vueDelimeters = ['${', '}'];
Vue.component('repo-search', {
delimiters: vueDelimeters,
props: {
searchLimit: {
type: Number,
default: 10
},
suburl: {
type: String,
required: true
},
uid: {
type: Number,
required: true
},
teamId: {
type: Number,
required: false,
default: 0
},
organizations: {
type: Array,
default: () => [],
},
isOrganization: {
type: Boolean,
default: true
},
canCreateOrganization: {
type: Boolean,
default: false
},
organizationsTotalCount: {
type: Number,
default: 0
},
moreReposLink: {
type: String,
default: ''
}
},
data() {
const params = new URLSearchParams(window.location.search);
let tab = params.get('repo-search-tab');
if (!tab) {
tab = 'repos';
}
let reposFilter = params.get('repo-search-filter');
if (!reposFilter) {
reposFilter = 'all';
}
let privateFilter = params.get('repo-search-private');
if (!privateFilter) {
privateFilter = 'both';
}
let archivedFilter = params.get('repo-search-archived');
if (!archivedFilter) {
archivedFilter = 'unarchived';
}
let searchQuery = params.get('repo-search-query');
if (!searchQuery) {
searchQuery = '';
}
let page = 1;
try {
page = parseInt(params.get('repo-search-page'));
} catch {
// noop
}
if (!page) {
page = 1;
}
return {
tab,
repos: [],
reposTotalCount: 0,
reposFilter,
archivedFilter,
privateFilter,
page,
finalPage: 1,
searchQuery,
isLoading: false,
staticPrefix: AssetUrlPrefix,
counts: {},
repoTypes: {
all: {
searchMode: '',
},
forks: {
searchMode: 'fork',
},
mirrors: {
searchMode: 'mirror',
},
sources: {
searchMode: 'source',
},
collaborative: {
searchMode: 'collaborative',
},
}
};
},
computed: {
showMoreReposLink() {
return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
},
searchURL() {
return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
}&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
}${this.reposFilter !== 'all' ? '&exclusive=1' : ''
}${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
}${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
}`;
},
repoTypeCount() {
return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
}
},
mounted() {
this.changeReposFilter(this.reposFilter);
$(this.$el).find('.poping.up').popup();
$(this.$el).find('.dropdown').dropdown();
this.setCheckboxes();
Vue.nextTick(() => {
this.$refs.search.focus();
});
},
methods: {
changeTab(t) {
this.tab = t;
this.updateHistory();
},
setCheckboxes() {
switch (this.archivedFilter) {
case 'unarchived':
$('#archivedFilterCheckbox').checkbox('set unchecked');
break;
case 'archived':
$('#archivedFilterCheckbox').checkbox('set checked');
break;
case 'both':
$('#archivedFilterCheckbox').checkbox('set indeterminate');
break;
default:
this.archivedFilter = 'unarchived';
$('#archivedFilterCheckbox').checkbox('set unchecked');
break;
}
switch (this.privateFilter) {
case 'public':
$('#privateFilterCheckbox').checkbox('set unchecked');
break;
case 'private':
$('#privateFilterCheckbox').checkbox('set checked');
break;
case 'both':
$('#privateFilterCheckbox').checkbox('set indeterminate');
break;
default:
this.privateFilter = 'both';
$('#privateFilterCheckbox').checkbox('set indeterminate');
break;
}
},
changeReposFilter(filter) {
this.reposFilter = filter;
this.repos = [];
this.page = 1;
Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
updateHistory() {
const params = new URLSearchParams(window.location.search);
if (this.tab === 'repos') {
params.delete('repo-search-tab');
} else {
params.set('repo-search-tab', this.tab);
}
if (this.reposFilter === 'all') {
params.delete('repo-search-filter');
} else {
params.set('repo-search-filter', this.reposFilter);
}
if (this.privateFilter === 'both') {
params.delete('repo-search-private');
} else {
params.set('repo-search-private', this.privateFilter);
}
if (this.archivedFilter === 'unarchived') {
params.delete('repo-search-archived');
} else {
params.set('repo-search-archived', this.archivedFilter);
}
if (this.searchQuery === '') {
params.delete('repo-search-query');
} else {
params.set('repo-search-query', this.searchQuery);
}
if (this.page === 1) {
params.delete('repo-search-page');
} else {
params.set('repo-search-page', `${this.page}`);
}
const queryString = params.toString();
if (queryString) {
window.history.replaceState({}, '', `?${queryString}`);
} else {
window.history.replaceState({}, '', window.location.pathname);
}
},
toggleArchivedFilter() {
switch (this.archivedFilter) {
case 'both':
this.archivedFilter = 'unarchived';
break;
case 'unarchived':
this.archivedFilter = 'archived';
break;
case 'archived':
this.archivedFilter = 'both';
break;
default:
this.archivedFilter = 'unarchived';
break;
}
this.page = 1;
this.repos = [];
this.setCheckboxes();
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
togglePrivateFilter() {
switch (this.privateFilter) {
case 'both':
this.privateFilter = 'public';
break;
case 'public':
this.privateFilter = 'private';
break;
case 'private':
this.privateFilter = 'both';
break;
default:
this.privateFilter = 'both';
break;
}
this.page = 1;
this.repos = [];
this.setCheckboxes();
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
changePage(page) {
this.page = page;
if (this.page > this.finalPage) {
this.page = this.finalPage;
}
if (this.page < 1) {
this.page = 1;
}
this.repos = [];
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0);
this.searchRepos();
},
searchRepos() {
this.isLoading = true;
if (!this.reposTotalCount) {
const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
$.getJSON(totalCountSearchURL, (_result, _textStatus, request) => {
this.reposTotalCount = request.getResponseHeader('X-Total-Count');
});
}
const searchedMode = this.repoTypes[this.reposFilter].searchMode;
const searchedURL = this.searchURL;
const searchedQuery = this.searchQuery;
$.getJSON(searchedURL, (result, _textStatus, request) => {
if (searchedURL === this.searchURL) {
this.repos = result.data;
const count = request.getResponseHeader('X-Total-Count');
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
this.reposTotalCount = count;
}
Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count);
this.finalPage = Math.ceil(count / this.searchLimit);
this.updateHistory();
}
}).always(() => {
if (searchedURL === this.searchURL) {
this.isLoading = false;
}
});
},
repoIcon(repo) {
if (repo.fork) {
return 'octicon-repo-forked';
} else if (repo.mirror) {
return 'octicon-mirror';
} else if (repo.template) {
return `octicon-repo-template`;
} else if (repo.private) {
return 'octicon-lock';
} else if (repo.internal) {
return 'octicon-repo';
}
return 'octicon-repo';
}
}
});
}
function initCtrlEnterSubmit() {
$('.js-quick-submit').on('keydown', function (e) {
if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.keyCode === 13 || e.keyCode === 10)) {
@ -3476,31 +3113,6 @@ function initCtrlEnterSubmit() {
});
}
function initVueApp() {
const el = document.getElementById('app');
if (!el) {
return;
}
initVueComponents();
new Vue({
el,
delimiters: ['${', '}'],
components: {
ActivityTopAuthors,
},
data: () => {
return {
searchLimit: Number((document.querySelector('meta[name=_search_limit]') || {}).content),
suburl: AppSubUrl,
uid: Number((document.querySelector('meta[name=_context_uid]') || {}).content),
activityTopAuthors: window.ActivityTopAuthors || [],
};
},
});
}
function initIssueTimetracking() {
$(document).on('click', '.issue-add-time', () => {
$('.issue-start-time-modal').modal({
@ -3543,163 +3155,6 @@ function initBranchOrTagDropdown(selector) {
});
}
function initFilterBranchTagDropdown(selector) {
$(selector).each(function () {
const $dropdown = $(this);
const $data = $dropdown.find('.data');
const data = {
items: [],
mode: $data.data('mode'),
searchTerm: '',
noResults: '',
canCreateBranch: false,
menuVisible: false,
createTag: false,
active: 0
};
$data.find('.item').each(function () {
data.items.push({
name: $(this).text(),
url: $(this).data('url'),
branch: $(this).hasClass('branch'),
tag: $(this).hasClass('tag'),
selected: $(this).hasClass('selected')
});
});
$data.remove();
new Vue({
el: this,
delimiters: ['${', '}'],
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.canCreateBranch || !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() {
this.noResults = this.$el.getAttribute('data-no-results');
this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true';
document.body.addEventListener('click', (event) => {
if (this.$el.contains(event.target)) return;
if (this.menuVisible) {
Vue.set(this, 'menuVisible', false);
}
});
},
methods: {
selectItem(item) {
const prev = this.getSelected();
if (prev !== null) {
prev.selected = false;
}
item.selected = true;
window.location.href = item.url;
},
createNewBranch() {
if (!this.showCreateNewBranch) return;
$(this.$refs.newBranchForm).trigger('submit');
},
focusSearchField() {
Vue.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;
}
}
}
});
});
}
$('.commit-button').on('click', function (e) {
e.preventDefault();