mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-02 20:06:06 +01:00 
			
		
		
		
	Refactor dashboard repo list to Vue SFC (#23405)
Similar to #23394 The dashboard repo list mixes jQuery/Fomantic UI/Vue together, it's very diffcult to maintain and causes unfixable a11y problems. This PR uses two steps to refactor the repo list: 1. move `data-` attributes to JS object and use Vue data as much as possibled3adc0dcac2. move the code into a Vue SFC7ebe55df6eTotal: +516 −585 Screenshots: <details>    </details> --------- Co-authored-by: John Olheiser <john.olheiser@gmail.com>
This commit is contained in:
		@@ -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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										432
									
								
								web_src/js/components/DashboardRepoList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										432
									
								
								web_src/js/components/DashboardRepoList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,432 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <div v-if="!isOrganization" class="ui two item tabable menu">
 | 
			
		||||
      <a :class="{item: true, active: tab === 'repos'}" @click="changeTab('repos')">{{ textRepository }}</a>
 | 
			
		||||
      <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
 | 
			
		||||
      <h4 class="ui top attached header gt-df gt-ac">
 | 
			
		||||
        <div class="gt-f1 gt-df gt-ac">
 | 
			
		||||
          {{ textMyRepos }}
 | 
			
		||||
          <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a class="tooltip" :href="subUrl + '/repo/create'" :data-content="textNewRepo" data-position="left center">
 | 
			
		||||
          <svg-icon name="octicon-plus"/>
 | 
			
		||||
          <span class="sr-only">{{ textNewRepo }}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </h4>
 | 
			
		||||
      <div class="ui attached segment repos-search">
 | 
			
		||||
        <div class="ui fluid right action left icon input" :class="{loading: isLoading}">
 | 
			
		||||
          <input @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" :placeholder="textSearchRepos">
 | 
			
		||||
          <i class="icon gt-df gt-ac gt-jc"><svg-icon name="octicon-search" :size="16"/></i>
 | 
			
		||||
          <div class="ui dropdown icon button" :title="textFilter">
 | 
			
		||||
            <i class="icon gt-df gt-ac gt-jc gt-m-0"><svg-icon name="octicon-filter" :size="16"/></i>
 | 
			
		||||
            <div class="menu">
 | 
			
		||||
              <a class="item" @click="toggleArchivedFilter()">
 | 
			
		||||
                <div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
 | 
			
		||||
                  <!--the "hidden" is necessary to make the checkbox work without Fomantic UI js,
 | 
			
		||||
                      otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
 | 
			
		||||
                  <input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps">
 | 
			
		||||
                  <label>
 | 
			
		||||
                    <svg-icon name="octicon-archive" :size="16" class-name="gt-mr-2"/>
 | 
			
		||||
                    {{ textShowArchived }}
 | 
			
		||||
                  </label>
 | 
			
		||||
                </div>
 | 
			
		||||
              </a>
 | 
			
		||||
              <a class="item" @click="togglePrivateFilter()">
 | 
			
		||||
                <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
 | 
			
		||||
                  <input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps">
 | 
			
		||||
                  <label>
 | 
			
		||||
                    <svg-icon name="octicon-lock" :size="16" class-name="gt-mr-2"/>
 | 
			
		||||
                    {{ textShowPrivate }}
 | 
			
		||||
                  </label>
 | 
			
		||||
                </div>
 | 
			
		||||
              </a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="ui secondary tiny pointing borderless menu center grid repos-filter">
 | 
			
		||||
          <a class="item" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
 | 
			
		||||
            {{ textAll }}
 | 
			
		||||
            <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
 | 
			
		||||
          </a>
 | 
			
		||||
          <a class="item" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
 | 
			
		||||
            {{ textSources }}
 | 
			
		||||
            <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
 | 
			
		||||
          </a>
 | 
			
		||||
          <a class="item" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
 | 
			
		||||
            {{ textForks }}
 | 
			
		||||
            <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
 | 
			
		||||
          </a>
 | 
			
		||||
          <a class="item" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
 | 
			
		||||
            {{ textMirrors }}
 | 
			
		||||
            <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
 | 
			
		||||
          </a>
 | 
			
		||||
          <a class="item" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
 | 
			
		||||
            {{ textCollaborative }}
 | 
			
		||||
            <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-if="repos.length" class="ui attached table segment gt-rounded-bottom">
 | 
			
		||||
        <ul class="repo-owner-name-list">
 | 
			
		||||
          <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"/>
 | 
			
		||||
                <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"/>
 | 
			
		||||
                </span>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="text light grey gt-df gt-ac" v-if="isStarsEnabled">
 | 
			
		||||
                {{ repo.stars_count }}
 | 
			
		||||
                <svg-icon name="octicon-star" :size="16" class-name="gt-ml-2"/>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <div v-if="showMoreReposLink" class="center gt-py-3 gt-border-secondary-top">
 | 
			
		||||
          <div class="ui borderless pagination menu narrow">
 | 
			
		||||
            <a
 | 
			
		||||
              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
 | 
			
		||||
              @click="changePage(1)" :title="textFirstPage"
 | 
			
		||||
            >
 | 
			
		||||
              <svg-icon name="gitea-double-chevron-left" :size="16" class-name="gt-mr-2"/>
 | 
			
		||||
            </a>
 | 
			
		||||
            <a
 | 
			
		||||
              class="item navigation gt-py-2" :class="{'disabled': page === 1}"
 | 
			
		||||
              @click="changePage(page - 1)" :title="textPreviousPage"
 | 
			
		||||
            >
 | 
			
		||||
              <svg-icon name="octicon-chevron-left" :size="16" clsas-name="gt-mr-2"/>
 | 
			
		||||
            </a>
 | 
			
		||||
            <a class="active item gt-py-2">{{ page }}</a>
 | 
			
		||||
            <a
 | 
			
		||||
              class="item navigation" :class="{'disabled': page === finalPage}"
 | 
			
		||||
              @click="changePage(page + 1)" :title="textNextPage"
 | 
			
		||||
            >
 | 
			
		||||
              <svg-icon name="octicon-chevron-right" :size="16" class-name="gt-ml-2"/>
 | 
			
		||||
            </a>
 | 
			
		||||
            <a
 | 
			
		||||
              class="item navigation gt-py-2" :class="{'disabled': page === finalPage}"
 | 
			
		||||
              @click="changePage(finalPage)" :title="textLastPage"
 | 
			
		||||
            >
 | 
			
		||||
              <svg-icon name="gitea-double-chevron-right" :size="16" class-name="gt-ml-2"/>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
 | 
			
		||||
      <h4 class="ui top attached header gt-df gt-ac">
 | 
			
		||||
        <div class="gt-f1 gt-df gt-ac">
 | 
			
		||||
          {{ textMyOrgs }}
 | 
			
		||||
          <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a v-if="canCreateOrganization" class="tooltip" :href="subUrl + '/org/create'" :data-content="textNewOrg" data-position="left center">
 | 
			
		||||
          <svg-icon name="octicon-plus"/>
 | 
			
		||||
          <span class="sr-only">{{ textNewOrg }}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </h4>
 | 
			
		||||
      <div v-if="organizations.length" class="ui attached table segment gt-rounded-bottom">
 | 
			
		||||
        <ul class="repo-owner-name-list">
 | 
			
		||||
          <li v-for="org in organizations" :key="org.name">
 | 
			
		||||
            <a class="repo-list-link gt-df gt-ac gt-sb" :href="subUrl + '/' + encodeURIComponent(org.name)">
 | 
			
		||||
              <div class="text truncate item-name gt-f1">
 | 
			
		||||
                <svg-icon name="octicon-organization" :size="16" class-name="gt-mr-2"/>
 | 
			
		||||
                <strong>{{ org.name }}</strong>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="text light grey gt-df gt-ac">
 | 
			
		||||
                {{ org.num_repos }}
 | 
			
		||||
                <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/>
 | 
			
		||||
              </div>
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {createApp, nextTick} from 'vue';
 | 
			
		||||
import $ from 'jquery';
 | 
			
		||||
import {initTooltip} from '../modules/tippy.js';
 | 
			
		||||
import {SvgIcon} from '../svg.js';
 | 
			
		||||
 | 
			
		||||
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
 | 
			
		||||
 | 
			
		||||
const sfc = {
 | 
			
		||||
  components: {SvgIcon},
 | 
			
		||||
  data() {
 | 
			
		||||
    const params = new URLSearchParams(window.location.search);
 | 
			
		||||
    const tab = params.get('repo-search-tab') || 'repos';
 | 
			
		||||
    const reposFilter = params.get('repo-search-filter') || 'all';
 | 
			
		||||
    const privateFilter = params.get('repo-search-private') || 'both';
 | 
			
		||||
    const archivedFilter = params.get('repo-search-archived') || 'unarchived';
 | 
			
		||||
    const searchQuery = params.get('repo-search-query') || '';
 | 
			
		||||
    const page = Number(params.get('repo-search-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',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      textArchivedFilterTitles: {},
 | 
			
		||||
      textPrivateFilterTitles: {},
 | 
			
		||||
 | 
			
		||||
      organizations: [],
 | 
			
		||||
      isOrganization: true,
 | 
			
		||||
      canCreateOrganization: false,
 | 
			
		||||
      organizationsTotalCount: 0,
 | 
			
		||||
 | 
			
		||||
      subUrl: appSubUrl,
 | 
			
		||||
      ...pageData.dashboardRepoList,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  computed: {
 | 
			
		||||
    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.textArchivedFilterTitles[this.archivedFilter];
 | 
			
		||||
    },
 | 
			
		||||
    checkboxArchivedFilterProps() {
 | 
			
		||||
      return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
 | 
			
		||||
    },
 | 
			
		||||
    checkboxPrivateFilterTitle() {
 | 
			
		||||
      return this.textPrivateFilterTitles[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.textArchivedFilterTitles = {
 | 
			
		||||
      'archived': this.textShowOnlyArchived,
 | 
			
		||||
      'unarchived': this.textShowOnlyUnarchived,
 | 
			
		||||
      'both': this.textShowBothArchivedUnarchived,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.textPrivateFilterTitles = {
 | 
			
		||||
      'private': this.textShowOnlyPrivate,
 | 
			
		||||
      'public': this.textShowOnlyPublic,
 | 
			
		||||
      'both': this.textShowBothPrivatePublic,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  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';
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function initDashboardRepoList() {
 | 
			
		||||
  const el = document.getElementById('dashboard-repo-list');
 | 
			
		||||
  if (el) {
 | 
			
		||||
    createApp(sfc).mount(el);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default sfc; // activate the IDE's Vue plugin
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
@@ -51,7 +51,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import VueBarGraph from 'vue-bar-graph';
 | 
			
		||||
import {initVueApp} from './VueComponentLoader.js';
 | 
			
		||||
import {createApp} from 'vue';
 | 
			
		||||
 | 
			
		||||
const sfc = {
 | 
			
		||||
  components: {VueBarGraph},
 | 
			
		||||
@@ -102,8 +102,11 @@ const sfc = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function initRepoActivityTopAuthorsChart() {
 | 
			
		||||
  initVueApp('#repo-activity-top-authors-chart', sfc);
 | 
			
		||||
  const el = document.getElementById('repo-activity-top-authors-chart');
 | 
			
		||||
  if (el) {
 | 
			
		||||
    createApp(sfc).mount(el);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default sfc; // this line is necessary to activate the IDE's Vue plugin
 | 
			
		||||
export default sfc; // activate the IDE's Vue plugin
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
@@ -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. <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"');
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user