diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index 97234176b..0a8f427f9 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -1,181 +1,54 @@
-
-
-
+
+
+
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js
deleted file mode 100644
index 2328cc83a..000000000
--- a/web_src/js/components/DashboardRepoList.js
+++ /dev/null
@@ -1,345 +0,0 @@
-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);
-}
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
new file mode 100644
index 000000000..e295910fd
--- /dev/null
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -0,0 +1,432 @@
+
+
+
+
+
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index 37b6df918..294ee6f7b 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -51,7 +51,7 @@
diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js
index e1bf35c12..a8945b82d 100644
--- a/web_src/js/components/RepoBranchTagDropdown.js
+++ b/web_src/js/components/RepoBranchTagDropdown.js
@@ -1,6 +1,5 @@
import {createApp, nextTick} from 'vue';
import $ from 'jquery';
-import {vueDelimiters} from './VueComponentLoader.js';
export function initRepoBranchTagDropdown(selector) {
$(selector).each(function (dropdownIndex, elRoot) {
@@ -39,7 +38,7 @@ export function initRepoBranchTagDropdown(selector) {
}
const view = createApp({
- delimiters: vueDelimiters,
+ delimiters: ['${', '}'],
data() {
return data;
},
diff --git a/web_src/js/components/VueComponentLoader.js b/web_src/js/components/VueComponentLoader.js
deleted file mode 100644
index 33ebf95ef..000000000
--- a/web_src/js/components/VueComponentLoader.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {createApp} from 'vue';
-import {svgs} from '../svg.js';
-
-export const vueDelimiters = ['${', '}'];
-
-let vueEnvInited = false;
-export function initVueEnv() {
- if (vueEnvInited) return;
- vueEnvInited = true;
-
- // As far as I could tell, this is no longer possible.
- // But there seem not to be a guide what to do instead.
- // const isProd = window.config.runModeIsProd;
- // Vue.config.devtools = !isProd;
-}
-
-let vueSvgInited = false;
-export function initVueSvg(app) {
- if (vueSvgInited) return;
- vueSvgInited = true;
-
- // register svg icon vue components, e.g.
- 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"');
-
- app.component(name, {
- props: {
- size: {
- type: String,
- default: '16',
- },
- },
- template,
- });
- }
-}
-
-export function initVueApp(el, opts = {}) {
- if (typeof el === 'string') {
- el = document.querySelector(el);
- }
- if (!el) return null;
-
- return createApp(
- {delimiters: vueDelimiters, ...opts}
- ).mount(el);
-}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 6b4f4ef3e..480661118 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -2,9 +2,8 @@
import './bootstrap.js';
import $ from 'jquery';
-import {initVueEnv} from './components/VueComponentLoader.js';
import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
-import {initDashboardRepoList} from './components/DashboardRepoList.js';
+import {initDashboardRepoList} from './components/DashboardRepoList.vue';
import {attachTribute} from './features/tribute.js';
import {initGlobalCopyToClipboardListener} from './features/clipboard.js';
@@ -100,7 +99,6 @@ $.fn.tab.settings.silent = true;
// Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element.
$.fn.checkbox.settings.enableEnterKey = false;
-initVueEnv();
$(document).ready(() => {
initGlobalCommon();
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 6476f16bf..9eabca3fd 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -31,8 +31,17 @@ import octiconSkip from '../../public/img/svg/octicon-skip.svg';
import octiconMeter from '../../public/img/svg/octicon-meter.svg';
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
import octiconSync from '../../public/img/svg/octicon-sync.svg';
+import octiconFilter from '../../public/img/svg/octicon-filter.svg';
+import octiconPlus from '../../public/img/svg/octicon-plus.svg';
+import octiconSearch from '../../public/img/svg/octicon-search.svg';
+import octiconArchive from '../../public/img/svg/octicon-archive.svg';
+import octiconStar from '../../public/img/svg/octicon-star.svg';
+import giteaDoubleChevronLeft from '../../public/img/svg/gitea-double-chevron-left.svg';
+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';
-export const svgs = {
+const svgs = {
'octicon-blocked': octiconBlocked,
'octicon-check-circle-fill': octiconCheckCircleFill,
'octicon-chevron-down': octiconChevronDown,
@@ -66,14 +75,25 @@ export const svgs = {
'octicon-triangle-down': octiconTriangleDown,
'octicon-x': octiconX,
'octicon-x-circle-fill': octiconXCircleFill,
+ 'octicon-filter': octiconFilter,
+ 'octicon-plus': octiconPlus,
+ 'octicon-search': octiconSearch,
+ 'octicon-archive': octiconArchive,
+ 'octicon-star': octiconStar,
+ 'gitea-double-chevron-left': giteaDoubleChevronLeft,
+ 'gitea-double-chevron-right': giteaDoubleChevronRight,
+ 'octicon-chevron-left': octiconChevronLeft,
+ 'octicon-organization': octiconOrganization,
};
+// 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();
-// retrieve a HTML string for given SVG icon name, size and additional classes
+// retrieve an HTML string for given SVG icon name, size and additional classes
export function svg(name, size = 16, className = '') {
- if (!(name in svgs)) return '';
+ if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
if (size === 16 && !className) return svgs[name];
const document = parser.parseFromString(svgs[name], 'image/svg+xml');