mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-29 15:13:53 +03:00
7a5af25592
Co-author: yp05327 , this PR is based on yp05327's #22813. The problems of the old DashboardRepoList / repolist.tmpl: * It mixes many different frameworks together * It "just works", bug on bug * It uses many anti-pattern of Vue This PR: * Fix bugs and close #22800 * Decouple the "checkbox" elements from Fomantic UI (only use CSS styles) * Simplify the HTML layout * Simplify JS logic * Make it easier to refactor the DashboardRepoList into a pure Vue component in the future. ### Screenshots #### Default ![image](https://user-images.githubusercontent.com/2114189/221355768-a3eb5b23-85b4-4e3d-b906-844d8b15539d.png) #### Click "Archived" to make it checked ![image](https://user-images.githubusercontent.com/2114189/221355777-9a104ddf-52a7-4504-869a-43a73827d802.png) #### Click "Archived" to make it intermediate ![image](https://user-images.githubusercontent.com/2114189/221355802-0f67a073-67ad-4e92-84a6-558c432103a5.png) #### Click "Archived" to make it unchecked ![image](https://user-images.githubusercontent.com/2114189/221355810-acf1d9d8-ccce-47fe-a02e-70cf4e666331.png) --------- Co-authored-by: yp05327 <576951401@qq.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
345 lines
9.9 KiB
JavaScript
345 lines
9.9 KiB
JavaScript
import {createApp, nextTick} from 'vue';
|
|
import $ from 'jquery';
|
|
import {initVueSvg, vueDelimiters} from './VueComponentLoader.js';
|
|
import {initTooltip} from '../modules/tippy.js';
|
|
|
|
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
|
|
|
function initVueComponents(app) {
|
|
app.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 {
|
|
hasMounted: false, // accessing $refs in computed() need to wait for mounted
|
|
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}/repo/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}`];
|
|
},
|
|
checkboxArchivedFilterTitle() {
|
|
return this.hasMounted && this.$refs.checkboxArchivedFilter?.getAttribute(`data-title-${this.archivedFilter}`);
|
|
},
|
|
checkboxArchivedFilterProps() {
|
|
return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
|
|
},
|
|
checkboxPrivateFilterTitle() {
|
|
return this.hasMounted && this.$refs.checkboxPrivateFilter?.getAttribute(`data-title-${this.privateFilter}`);
|
|
},
|
|
checkboxPrivateFilterProps() {
|
|
return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
const el = document.getElementById('dashboard-repo-list');
|
|
this.changeReposFilter(this.reposFilter);
|
|
for (const elTooltip of el.querySelectorAll('.tooltip')) {
|
|
initTooltip(elTooltip);
|
|
}
|
|
$(el).find('.dropdown').dropdown();
|
|
nextTick(() => {
|
|
this.$refs.search.focus();
|
|
});
|
|
|
|
this.hasMounted = true;
|
|
},
|
|
|
|
methods: {
|
|
changeTab(t) {
|
|
this.tab = t;
|
|
this.updateHistory();
|
|
},
|
|
|
|
changeReposFilter(filter) {
|
|
this.reposFilter = filter;
|
|
this.repos = [];
|
|
this.page = 1;
|
|
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() {
|
|
if (this.archivedFilter === 'unarchived') {
|
|
this.archivedFilter = 'archived';
|
|
} else if (this.archivedFilter === 'archived') {
|
|
this.archivedFilter = 'both';
|
|
} else { // including both
|
|
this.archivedFilter = 'unarchived';
|
|
}
|
|
this.page = 1;
|
|
this.repos = [];
|
|
this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
|
|
this.searchRepos();
|
|
},
|
|
|
|
togglePrivateFilter() {
|
|
if (this.privateFilter === 'both') {
|
|
this.privateFilter = 'public';
|
|
} else if (this.privateFilter === 'public') {
|
|
this.privateFilter = 'private';
|
|
} else { // including private
|
|
this.privateFilter = 'both';
|
|
}
|
|
this.page = 1;
|
|
this.repos = [];
|
|
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 = [];
|
|
this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
|
|
this.searchRepos();
|
|
},
|
|
|
|
async searchRepos() {
|
|
this.isLoading = true;
|
|
|
|
const searchedMode = this.repoTypes[this.reposFilter].searchMode;
|
|
const searchedURL = this.searchURL;
|
|
const searchedQuery = this.searchQuery;
|
|
|
|
let response, json;
|
|
try {
|
|
if (!this.reposTotalCount) {
|
|
const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
|
|
response = await fetch(totalCountSearchURL);
|
|
this.reposTotalCount = response.headers.get('X-Total-Count');
|
|
}
|
|
|
|
response = await fetch(searchedURL);
|
|
json = await response.json();
|
|
} catch {
|
|
if (searchedURL === this.searchURL) {
|
|
this.isLoading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (searchedURL === this.searchURL) {
|
|
this.repos = json.data;
|
|
const count = response.headers.get('X-Total-Count');
|
|
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
|
|
this.reposTotalCount = count;
|
|
}
|
|
this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count;
|
|
this.finalPage = Math.ceil(count / this.searchLimit);
|
|
this.updateHistory();
|
|
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';
|
|
}
|
|
},
|
|
|
|
template: document.getElementById('dashboard-repo-list-template'),
|
|
});
|
|
}
|
|
|
|
export function initDashboardRepoList() {
|
|
const el = document.getElementById('dashboard-repo-list');
|
|
const dashboardRepoListData = pageData.dashboardRepoList || null;
|
|
if (!el || !dashboardRepoListData) return;
|
|
|
|
const app = createApp({
|
|
delimiters: vueDelimiters,
|
|
data() {
|
|
return {
|
|
searchLimit: dashboardRepoListData.searchLimit || 0,
|
|
subUrl: appSubUrl,
|
|
uid: dashboardRepoListData.uid || 0,
|
|
};
|
|
},
|
|
});
|
|
initVueSvg(app);
|
|
initVueComponents(app);
|
|
app.mount(el);
|
|
}
|