mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-30 19:08:37 +00: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,181 +1,54 @@ | ||||
| <div id="dashboard-repo-list" class="six wide column"> | ||||
| 	<repo-search | ||||
| 	:search-limit="searchLimit" | ||||
| 	:sub-url="subUrl" | ||||
| 	:uid="uid" | ||||
| 	{{if .Team}} | ||||
| 	:team-id="{{.Team.ID}}" | ||||
| 	{{end}} | ||||
| 	:more-repos-link="'{{.ContextUser.HomeLink}}'" | ||||
| 	{{if not .ContextUser.IsOrganization}} | ||||
| 	:organizations="[ | ||||
| 	{{range .Orgs}} | ||||
| 	{name: '{{.Name}}', num_repos: '{{.NumRepos}}'}, | ||||
| 	{{end}} | ||||
| 	]" | ||||
| 	:is-organization="false" | ||||
| 	:organizations-total-count="{{.UserOrgsCount}}" | ||||
| 	:can-create-organization="{{.SignedUser.CanCreateOrganization}}" | ||||
| 	{{end}} | ||||
| 	inline-template | ||||
| 	v-cloak | ||||
| 	></repo-search> | ||||
| </div> | ||||
| <script type="module"> | ||||
| const data = { | ||||
| 	...window.config.pageData.dashboardRepoList, // it only contains searchLimit and uid | ||||
|  | ||||
| <template id="dashboard-repo-list-template"> | ||||
| 	<div> | ||||
| 		<div v-if="!isOrganization" class="ui two item tabable menu"> | ||||
| 			<a :class="{item: true, active: tab === 'repos'}" @click="changeTab('repos')">{{.locale.Tr "repository"}}</a> | ||||
| 			<a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{.locale.Tr "organization"}}</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"> | ||||
| 					{{.locale.Tr "home.my_repos"}} | ||||
| 					<span class="ui grey label gt-ml-3">${reposTotalCount}</span> | ||||
| 				</div> | ||||
| 				<a class="tooltip" :href="subUrl + '/repo/create'" data-content="{{.locale.Tr "new_repo"}}" data-position="left center"> | ||||
| 					{{svg "octicon-plus"}} | ||||
| 					<span class="sr-only">{{.locale.Tr "new_repo"}}</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="{{.locale.Tr "home.search_repos"}}"> | ||||
| 					<i class="icon gt-df gt-ac gt-jc">{{svg "octicon-search" 16}}</i> | ||||
| 					<div class="ui dropdown icon button" title="{{.locale.Tr "home.filter"}}"> | ||||
| 						<i class="icon gt-df gt-ac gt-jc gt-m-0">{{svg "octicon-filter" 16}}</i> | ||||
| 						<div class="menu"> | ||||
| 							<a class="item" @click="toggleArchivedFilter()"> | ||||
| 								<div class="ui checkbox" | ||||
| 											ref="checkboxArchivedFilter" | ||||
| 											data-title-both="{{.locale.Tr "home.show_both_archived_unarchived"}}" | ||||
| 											data-title-unarchived="{{.locale.Tr "home.show_only_unarchived"}}" | ||||
| 											data-title-archived="{{.locale.Tr "home.show_only_archived"}}" | ||||
| 											: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 "octicon-archive" 16 "gt-mr-2"}} | ||||
| 										{{.locale.Tr "home.show_archived"}} | ||||
| 									</label> | ||||
| 								</div> | ||||
| 							</a> | ||||
| 							<a class="item" @click="togglePrivateFilter()"> | ||||
| 								<div class="ui checkbox" | ||||
| 											ref="checkboxPrivateFilter" | ||||
| 											data-title-both="{{.locale.Tr "home.show_both_private_public"}}" | ||||
| 											data-title-public="{{.locale.Tr "home.show_only_public"}}" | ||||
| 											data-title-private="{{.locale.Tr "home.show_only_private"}}" | ||||
| 											:title="checkboxPrivateFilterTitle" | ||||
| 								> | ||||
| 									<input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps"> | ||||
| 									<label> | ||||
| 										{{svg "octicon-lock" 16 "gt-mr-2"}} | ||||
| 										{{.locale.Tr "home.show_private"}} | ||||
| 									</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')"> | ||||
| 						{{.locale.Tr "all"}} | ||||
| 						<div v-show="reposFilter === 'all'" class="ui circular mini grey label">${repoTypeCount}</div> | ||||
| 					</a> | ||||
| 					<a class="item" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')"> | ||||
| 						{{.locale.Tr "sources"}} | ||||
| 						<div v-show="reposFilter === 'sources'" class="ui circular mini grey label">${repoTypeCount}</div> | ||||
| 					</a> | ||||
| 					<a class="item" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')"> | ||||
| 						{{.locale.Tr "forks"}} | ||||
| 						<div v-show="reposFilter === 'forks'" class="ui circular mini grey label">${repoTypeCount}</div> | ||||
| 					</a> | ||||
| 					{{if .MirrorsEnabled}} | ||||
| 					<a class="item" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')"> | ||||
| 						{{.locale.Tr "mirrors"}} | ||||
| 						<div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">${repoTypeCount}</div> | ||||
| 					</a> | ||||
| 					{{end}} | ||||
| 					<a class="item" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')"> | ||||
| 						{{.locale.Tr "collaborative"}} | ||||
| 						<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}"> | ||||
| 						<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"> | ||||
| 								<component v-bind:is="repoIcon(repo)" size="16" class="gt-mr-2"></component> | ||||
| 								<div class="text gt-bold truncate gt-ml-1">${repo.full_name}</div> | ||||
| 								<span v-if="repo.archived"> | ||||
| 									{{svg "octicon-archive" 16 "gt-ml-2"}} | ||||
| 								</span> | ||||
| 							</div> | ||||
| 							{{if not .DisableStars}} | ||||
| 								<div class="text light grey gt-df gt-ac"> | ||||
| 									${repo.stars_count} | ||||
| 									{{svg "octicon-star" 16 "gt-ml-2"}} | ||||
| 								</div> | ||||
| 							{{end}} | ||||
| 						</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="{{$.locale.Tr "admin.first_page"}}"> | ||||
| 							{{svg "gitea-double-chevron-left" 16 "gt-mr-2"}} | ||||
| 						</a> | ||||
| 						<a class="item navigation gt-py-2" :class="{'disabled': page === 1}" | ||||
| 							@click="changePage(page - 1)" title="{{$.locale.Tr "repo.issues.previous"}}"> | ||||
| 							{{svg "octicon-chevron-left" 16 "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="{{$.locale.Tr "repo.issues.next"}}"> | ||||
| 							{{svg "octicon-chevron-right" 16 "gt-ml-2"}} | ||||
| 						</a> | ||||
| 						<a class="item navigation gt-py-2" :class="{'disabled': page === finalPage}" | ||||
| 							@click="changePage(finalPage)" title="{{$.locale.Tr "admin.last_page"}}"> | ||||
| 							{{svg "gitea-double-chevron-right" 16 "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"> | ||||
| 					{{.locale.Tr "home.my_orgs"}} | ||||
| 					<span class="ui grey label gt-ml-3">${organizationsTotalCount}</span> | ||||
| 				</div> | ||||
| 				<a v-if="canCreateOrganization" class="tooltip" :href="subUrl + '/org/create'" data-content="{{.locale.Tr "new_org"}}" data-position="left center"> | ||||
| 					{{svg "octicon-plus"}} | ||||
| 					<span class="sr-only">{{.locale.Tr "new_org"}}</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"> | ||||
| 						<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 "octicon-organization" 16 "gt-mr-2"}} | ||||
| 								<strong>${org.name}</strong> | ||||
| 							</div> | ||||
| 							<div class="text light grey gt-df gt-ac"> | ||||
| 								${org.num_repos} | ||||
| 								{{svg "octicon-repo" 16 "gt-ml-2 gt-mt-1"}} | ||||
| 							</div> | ||||
| 						</a> | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </template> | ||||
| 	isMirrorsEnabled: {{.IsMirrorsEnabled}}, | ||||
| 	isStarsEnabled: {{not .IsDisableStars}}, | ||||
|  | ||||
| 	textRepository: {{.locale.Tr "repository"}}, | ||||
| 	textOrganization: {{.locale.Tr "organization"}}, | ||||
| 	textMyRepos: {{.locale.Tr "home.my_repos"}}, | ||||
| 	textNewRepo: {{.locale.Tr "new_repo"}}, | ||||
| 	textSearchRepos: {{.locale.Tr "home.search_repos"}}, | ||||
| 	textFilter: {{.locale.Tr "home.filter"}}, | ||||
| 	textShowArchived: {{.locale.Tr "home.show_archived"}}, | ||||
| 	textShowPrivate: {{.locale.Tr "home.show_private"}}, | ||||
|  | ||||
| 	textShowBothArchivedUnarchived: {{.locale.Tr "home.show_both_archived_unarchived"}}, | ||||
| 	textShowOnlyUnarchived: {{.locale.Tr "home.show_only_unarchived"}}, | ||||
| 	textShowOnlyArchived: {{.locale.Tr "home.show_only_archived"}}, | ||||
|  | ||||
| 	textShowBothPrivatePublic: {{.locale.Tr "home.show_both_private_public"}}, | ||||
| 	textShowOnlyPublic: {{.locale.Tr "home.show_only_public"}}, | ||||
| 	textShowOnlyPrivate: {{.locale.Tr "home.show_only_private"}}, | ||||
|  | ||||
| 	textAll: {{.locale.Tr "all"}}, | ||||
| 	textSources: {{.locale.Tr "sources"}}, | ||||
| 	textForks: {{.locale.Tr "forks"}}, | ||||
| 	textMirrors: {{.locale.Tr "mirrors"}}, | ||||
| 	textCollaborative: {{.locale.Tr "collaborative"}}, | ||||
|  | ||||
| 	textFirstPage: {{.locale.Tr "admin.first_page"}}, | ||||
| 	textPreviousPage: {{.locale.Tr "repo.issues.previous"}}, | ||||
| 	textNextPage: {{.locale.Tr "repo.issues.next"}}, | ||||
| 	textLastPage: {{.locale.Tr "admin.last_page"}}, | ||||
|  | ||||
| 	textMyOrgs: {{.locale.Tr "home.my_orgs"}}, | ||||
| 	textNewOrg: {{.locale.Tr "new_org"}}, | ||||
| }; | ||||
|  | ||||
| {{if .Team}} | ||||
| data.teamId = {{.Team.ID}}; | ||||
| {{end}} | ||||
|  | ||||
| {{if not .ContextUser.IsOrganization}} | ||||
| data.organizations = [{{range .Orgs}}{'name': {{.Name}}, 'num_repos': {{.NumRepos}}},{{end}}]; | ||||
| data.isOrganization = false; | ||||
| data.organizationsTotalCount = {{.UserOrgsCount}} | ||||
| data.canCreateOrganization = {{.SignedUser.CanCreateOrganization}} | ||||
| {{end}} | ||||
|  | ||||
| window.config.pageData.dashboardRepoList = data; | ||||
| </script> | ||||
|  | ||||
| <div id="dashboard-repo-list" class="six wide column"></div> | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user